Skip to content

Commit

Permalink
[MinimumPrice][ApplyOnDiscounted] merge solutions for apply on discou…
Browse files Browse the repository at this point in the history
…nt and minimum prices
  • Loading branch information
SirDomin committed Dec 9, 2021
1 parent e735927 commit 9ce4cb8
Show file tree
Hide file tree
Showing 13 changed files with 147 additions and 31 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"
10 changes: 10 additions & 0 deletions src/Sylius/Behat/Context/Setup/PromotionContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,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
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
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,15 @@ public function execute(PromotionSubjectInterface $subject, array $configuration
return false;
}

$subjectTotal = $promotion->getAppliesToDiscounted() ? $subject->getPromotionSubjectTotal() : $subject->getNonDiscountedItemsTotal();
$subjectTotal = $this->getSubjectTotal($subject, $promotion);
$promotionAmount = $this->calculateAdjustmentAmount($subjectTotal, $configuration[$channelCode]['amount']);

if (0 === $promotionAmount) {
return false;
}

if ($this->minimumPriceDistributor !== null) {
$splitPromotion = $this->minimumPriceDistributor->distribute($subject->getItems()->toArray(), $promotionAmount, $subject->getChannel());
$splitPromotion = $this->minimumPriceDistributor->distribute($subject->getItems()->toArray(), $promotionAmount, $subject->getChannel(), $promotion->getAppliesToDiscounted());
} else {
$itemsTotal = [];
foreach ($subject->getItems() as $orderItem) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ public function execute(PromotionSubjectInterface $subject, array $configuration
return false;
}

$subjectTotal = $promotion->getAppliesToDiscounted() ? $subject->getPromotionSubjectTotal() : $subject->getNonDiscountedItemsTotal();
$subjectTotal = $this->getSubjectTotal($subject, $promotion);
$promotionAmount = $this->calculateAdjustmentAmount($subjectTotal, $configuration['percentage']);

if (0 === $promotionAmount) {
return false;
}

if ($this->minimumPriceDistributor !== null) {
$splitPromotion = $this->minimumPriceDistributor->distribute($subject->getItems()->toArray(), $promotionAmount, $subject->getChannel());
$splitPromotion = $this->minimumPriceDistributor->distribute($subject->getItems()->toArray(), $promotionAmount, $subject->getChannel(), $promotion->getAppliesToDiscounted());
} else {
$itemsTotal = [];
foreach ($subject->getItems() as $orderItem) {
Expand Down Expand Up @@ -104,4 +104,9 @@ private function calculateAdjustmentAmount(int $promotionSubjectTotal, float $pe
{
return -1 * (int) round($promotionSubjectTotal * $percentage);
}

private function getSubjectTotal(OrderInterface $order, PromotionInterface $promotion): int
{
return $promotion->getAppliesToDiscounted() ? $order->getPromotionSubjectTotal() : $order->getNonDiscountedItemsTotal();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,34 @@ function it_distributes_promotion_taking_into_account_minimum_price(
$tshirt->getVariant()->willReturn($tshirtVariant);
$tshirtVariant->getChannelPricingForChannel($channel)->willReturn($tshirtVariantChannelPricing);
$tshirtVariantChannelPricing->getMinimumPrice()->willReturn(0);
$tshirtVariant->getAppliedPromotionsForChannel($channel)->willReturn([]);

$book->getTotal()->willReturn(2000);
$book->getQuantity()->willReturn(1);
$book->getVariant()->willReturn($bookVariant);
$bookVariant->getChannelPricingForChannel($channel)->willReturn($bookVariantChannelPricing);
$bookVariantChannelPricing->getMinimumPrice()->willReturn(1900);
$bookVariant->getAppliedPromotionsForChannel($channel)->willReturn([]);

$shoes->getTotal()->willReturn(5000);
$shoes->getQuantity()->willReturn(1);
$shoes->getVariant()->willReturn($shoesVariant);
$shoesVariant->getChannelPricingForChannel($channel)->willReturn($shoesVariantChannelPricing);
$shoesVariantChannelPricing->getMinimumPrice()->willReturn(5000);
$shoesVariant->getAppliedPromotionsForChannel($channel)->willReturn([]);

$boardGame->getTotal()->willReturn(3000);
$boardGame->getQuantity()->willReturn(1);
$boardGame->getVariant()->willReturn($boardGameVariant);
$boardGameVariant->getChannelPricingForChannel($channel)->willReturn($boardGameVariantChannelPricing);
$boardGameVariantChannelPricing->getMinimumPrice()->willReturn(2600);
$boardGameVariant->getAppliedPromotionsForChannel($channel)->willReturn([]);

$proportionalIntegerDistributor->distribute([1000, 2000, 5000, 3000], -1200)->willReturn([-110, -218, -545, -327]);
$proportionalIntegerDistributor->distribute([1000, 3000], -663)->willReturn([-166, -497]);
$proportionalIntegerDistributor->distribute([1000], -424)->willReturn([-424]);

$this->distribute([$tshirt, $book, $shoes, $boardGame], -1200, $channel)->shouldReturn([-700, -100, 0, -400]);
$this->distribute([$tshirt, $book, $shoes, $boardGame], -1200, $channel, true)->shouldReturn([-700, -100, 0, -400]);
}

function it_distributes_promotion_taking_into_account_minimum_price_with_quantity(
Expand All @@ -89,17 +93,19 @@ function it_distributes_promotion_taking_into_account_minimum_price_with_quantit
$tshirt->getVariant()->willReturn($tshirtVariant);
$tshirtVariant->getChannelPricingForChannel($channel)->willReturn($tshirtVariantChannelPricing);
$tshirtVariantChannelPricing->getMinimumPrice()->willReturn(4500);
$tshirtVariant->getAppliedPromotionsForChannel($channel)->willReturn([]);

$mug->getTotal()->willReturn(6000);
$mug->getQuantity()->willReturn(3);
$mug->getVariant()->willReturn($mugVariant);
$mugVariant->getChannelPricingForChannel($channel)->willReturn($mugVariantChannelPricing);
$mugVariantChannelPricing->getMinimumPrice()->willReturn(0);
$mugVariant->getAppliedPromotionsForChannel($channel)->willReturn([]);

$proportionalIntegerDistributor->distribute([5000, 6000], -2500)->willReturn([-1136, -1364]);
$proportionalIntegerDistributor->distribute([6000], -636)->willReturn([-636]);

$this->distribute([$tshirt, $mug], -2500, $channel)->shouldReturn([-500, -2000]);
$this->distribute([$tshirt, $mug], -2500, $channel, true)->shouldReturn([-500, -2000]);
}

function it_distributes_promotion_that_exceeds_possible_distribution_taking_into_account_minimum_price(
Expand All @@ -117,16 +123,47 @@ function it_distributes_promotion_that_exceeds_possible_distribution_taking_into
$tshirt->getVariant()->willReturn($tshirtVariant);
$tshirtVariant->getChannelPricingForChannel($channel)->willReturn($tshirtVariantChannelPricing);
$tshirtVariantChannelPricing->getMinimumPrice()->willReturn(4500);
$tshirtVariant->getAppliedPromotionsForChannel($channel)->willReturn([]);

$mug->getTotal()->willReturn(6000);
$mug->getQuantity()->willReturn(3);
$mug->getVariant()->willReturn($mugVariant);
$mugVariant->getChannelPricingForChannel($channel)->willReturn($mugVariantChannelPricing);
$mugVariantChannelPricing->getMinimumPrice()->willReturn(1500);
$mugVariant->getAppliedPromotionsForChannel($channel)->willReturn([]);

$proportionalIntegerDistributor->distribute([5000, 6000], -2500)->willReturn([-1136, -1364]);
$proportionalIntegerDistributor->distribute([6000], -636)->willReturn([-636]);

$this->distribute([$tshirt, $mug], -2500, $channel)->shouldReturn([-500, -1500]);
$this->distribute([$tshirt, $mug], -2500, $channel, true)->shouldReturn([-500, -1500]);
}

function it_distributes_promotion_for_products_without_promotions_if_promotion_does_not_apply_on_catalog_promotions(
ProportionalIntegerDistributorInterface $proportionalIntegerDistributor,
OrderItemInterface $tshirt,
OrderItemInterface $mug,
ProductVariantInterface $tshirtVariant,
ProductVariantInterface $mugVariant,
ChannelPricingInterface $tshirtVariantChannelPricing,
ChannelPricingInterface $mugVariantChannelPricing,
ChannelInterface $channel
) {
$tshirt->getTotal()->willReturn(5000);
$tshirt->getQuantity()->willReturn(1);
$tshirt->getVariant()->willReturn($tshirtVariant);
$tshirtVariant->getChannelPricingForChannel($channel)->willReturn($tshirtVariantChannelPricing);
$tshirtVariantChannelPricing->getMinimumPrice()->willReturn(4500);
$tshirtVariant->getAppliedPromotionsForChannel($channel)->willReturn([['promotion_applied']]);

$mug->getTotal()->willReturn(6000);
$mug->getQuantity()->willReturn(3);
$mug->getVariant()->willReturn($mugVariant);
$mugVariant->getChannelPricingForChannel($channel)->willReturn($mugVariantChannelPricing);
$mugVariantChannelPricing->getMinimumPrice()->willReturn(1500);
$mugVariant->getAppliedPromotionsForChannel($channel)->willReturn([]);

$proportionalIntegerDistributor->distribute([5000, 6000], -2500)->willReturn([0, -1500]);

$this->distribute([$tshirt, $mug], -2500, $channel, true)->shouldReturn([0, -1500]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ function it_distributes_an_integer_even_if_its_indivisible_by_number_of_items():
$this->distribute([4300, 1400, 2300], -299)->shouldReturn([-161, -52, -86]);
}

function it_distributes_an_integer_even_for_non_distributable_items(): void
{
$this->distribute([0], -299)->shouldReturn([0]);
}

function it_throws_an_exception_if_any_of_integers_array_element_is_not_integer(): void
{
$this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ function it_uses_a_distributor_and_applicator_to_execute_promotion_action(
$secondItem->getTotal()->willReturn(4000);
$secondItem->getQuantity()->willReturn(1);

$minimumPriceDistributor->distribute([$firstItem, $secondItem], -1000, $channel)->willReturn([-600, -400]);
$minimumPriceDistributor->distribute([$firstItem, $secondItem], -1000, $channel, true)->willReturn([-600, -400]);
$unitsPromotionAdjustmentsApplicator->apply($order, $promotion, [-600, -400])->shouldBeCalled();

$this->execute($order, ['WEB_US' => ['amount' => 1000]], $promotion)->shouldReturn(true);
Expand Down Expand Up @@ -125,6 +125,8 @@ function it_distributes_promotion_using_regular_distributor_if_minimum_price_dis
->willReturn(new ArrayCollection([$firstItem->getWrappedObject(), $secondItem->getWrappedObject()]))
;

$promotion->getAppliesToDiscounted()->willReturn(true);

$firstItem->getVariant()->willReturn($productVariantOne);
$secondItem->getVariant()->willReturn($productVariantTwo);
$productVariantOne->getChannelPricingForChannel($channel)->willReturn($channelPricingOne);
Expand All @@ -139,7 +141,7 @@ function it_distributes_promotion_using_regular_distributor_if_minimum_price_dis
$secondItem->getTotal()->willReturn(4000);
$secondItem->getQuantity()->willReturn(1);

$minimumPriceDistributor->distribute([$firstItem, $secondItem], -1000, $channel)->shouldNotBeCalled();
$minimumPriceDistributor->distribute([$firstItem, $secondItem], -1000, $channel, true)->shouldNotBeCalled();
$distributor->distribute([6000, 4000], -1000)->willReturn([-200, -800]);

$unitsPromotionAdjustmentsApplicator->apply($order, $promotion, [-200, -800])->shouldBeCalled();
Expand Down Expand Up @@ -184,8 +186,9 @@ function it_distributes_promotion_up_to_minimum_price_of_variant(
$firstItem->getQuantity()->willReturn(1);
$secondItem->getTotal()->willReturn(4000);
$secondItem->getQuantity()->willReturn(1);
$promotion->getAppliesToDiscounted()->willReturn(true);

$minimumPriceDistributor->distribute([$firstItem, $secondItem], -1000, $channel)->willReturn([-200, -800]);
$minimumPriceDistributor->distribute([$firstItem, $secondItem], -1000, $channel, true)->willReturn([-200, -800]);

$unitsPromotionAdjustmentsApplicator->apply($order, $promotion, [-200, -800])->shouldBeCalled();

Expand Down Expand Up @@ -226,7 +229,7 @@ function it_uses_a_distributor_and_applicator_to_execute_promotion_action_only_f
$secondVariant->getAppliedPromotionsForChannel($channel)->willReturn(['winter_sale' => ['name' => 'Winter sale']]);

$proportionalIntegerDistributor->distribute([6000, 0], -1000)->shouldNotBeCalled();
$minimumPriceDistributor->distribute([$firstItem, $secondItem], -10000, $channel)->willReturn([-6000, 0]);
$minimumPriceDistributor->distribute([$firstItem, $secondItem], -1000, $channel, false)->willReturn([-1000, 0]);

$unitsPromotionAdjustmentsApplicator->apply($order, $promotion, [-1000, 0])->shouldBeCalled();

Expand Down Expand Up @@ -274,7 +277,7 @@ function it_does_not_apply_bigger_discount_than_promotion_subject_total(
$secondItem->getTotal()->willReturn(4000);
$secondItem->getQuantity()->willReturn(1);

$minimumPriceDistributor->distribute([$firstItem, $secondItem], -10000, $channel)->willReturn([-6000, -4000]);
$minimumPriceDistributor->distribute([$firstItem, $secondItem], -10000, $channel, true)->willReturn([-6000, -4000]);

$unitsPromotionAdjustmentsApplicator->apply($order, $promotion, [-6000, -4000])->shouldBeCalled();

Expand Down

0 comments on commit 9ce4cb8

Please sign in to comment.