diff --git a/src/Adder/DiscountAdjustmentsAdder.php b/src/Adder/DiscountAdjustmentsAdder.php new file mode 100644 index 0000000..e4d2aa8 --- /dev/null +++ b/src/Adder/DiscountAdjustmentsAdder.php @@ -0,0 +1,46 @@ +integerDistributor->distribute($discount, $orderItem->getQuantity()); + $units = $orderItem->getUnits(); + + /** @var int $discount */ + foreach ($discounts as $i => $discount) { + /** @var AdjustmentInterface $adjustment */ + $adjustment = $this->adjustmentFactory->createWithData( + $adjustmentType, + $label, + $discount, + ); + $adjustment->setOriginCode($originCode); + + /** @var OrderItemUnitInterface $unit */ + $unit = $units->get($i); + $unit->addAdjustment($adjustment); + } + } +} diff --git a/src/Adder/DiscountAdjustmentsAdderInterface.php b/src/Adder/DiscountAdjustmentsAdderInterface.php new file mode 100644 index 0000000..ea3a401 --- /dev/null +++ b/src/Adder/DiscountAdjustmentsAdderInterface.php @@ -0,0 +1,18 @@ + [ 'label' => false, ], - 'getter' => function (AdjustableInterface &$adjustable): array { - $adjustments = $adjustable->getAdjustments($this->adjustmentType)->toArray(); + 'getter' => + /** @param OrderItemInterface|OrderInterface $adjustable */ + function (AdjustableInterface &$adjustable): array { + Assert::isInstanceOfAny($adjustable, [OrderInterface::class, OrderItemInterface::class]); + /** @var Collection $adjustments */ + $adjustments = $adjustable->getAdjustmentsRecursively($this->adjustmentType); - return array_map(function (AdjustmentInterface $adjustment): int { - return -1 * $adjustment->getAmount(); - }, $adjustments); - }, + $notDistributedAdjustments = []; + /** @var AdjustmentInterface $adjustment */ + foreach ($adjustments as $adjustment) { + /** @var string $originCode */ + $originCode = $adjustment->getOriginCode(); + + if (isset($notDistributedAdjustments[$originCode])) { + $notDistributedAdjustments[$originCode] += ($adjustment->getAmount()) * -1; + + continue; + } + + $notDistributedAdjustments[$originCode] = ($adjustment->getAmount()) * -1; + } + + return $notDistributedAdjustments; + }, 'setter' => function (AdjustableInterface &$adjustable, array $discounts): void { - $adjustable->removeAdjustments($this->adjustmentType); - - /** @var int $discount */ - foreach ($discounts as $discount) { - $adjustment = $this->adjustmentFactory->createWithData( - $this->adjustmentType, - $this->label, - -1 * $discount, - ); - $adjustable->addAdjustment($adjustment); - } + $this->setDiscounts($adjustable, $discounts); }, ]); } + + abstract public function setDiscounts(AdjustableInterface $adjustable, array $discounts): void; } diff --git a/src/Form/Type/OrderDiscountCollectionType.php b/src/Form/Type/OrderDiscountCollectionType.php index 61ed3b8..5412234 100644 --- a/src/Form/Type/OrderDiscountCollectionType.php +++ b/src/Form/Type/OrderDiscountCollectionType.php @@ -5,12 +5,30 @@ namespace Setono\SyliusOrderEditPlugin\Form\Type; use Setono\SyliusOrderEditPlugin\Model\AdjustmentTypes; +use Setono\SyliusOrderEditPlugin\Setter\OrderDiscountAdjustmentSetterInterface; +use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Order\Factory\AdjustmentFactoryInterface; +use Sylius\Component\Order\Model\AdjustableInterface; +use Webmozart\Assert\Assert; final class OrderDiscountCollectionType extends CustomDiscountCollectionType { - public function __construct(AdjustmentFactoryInterface $adjustmentFactory) - { + public function __construct( + AdjustmentFactoryInterface $adjustmentFactory, + private readonly OrderDiscountAdjustmentSetterInterface $orderDiscountAdjustmentSetter, + ) { parent::__construct($adjustmentFactory, 'Custom discount', AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT); } + + public function setDiscounts(AdjustableInterface $adjustable, array $discounts): void + { + Assert::isInstanceOf($adjustable, OrderInterface::class); + + $adjustable->removeAdjustmentsRecursively($this->adjustmentType); + + /** @var int $discount */ + foreach ($discounts as $discount) { + $this->orderDiscountAdjustmentSetter->set($adjustable, $discount); + } + } } diff --git a/src/Form/Type/OrderItemDiscountCollectionType.php b/src/Form/Type/OrderItemDiscountCollectionType.php index a6ef476..9b49205 100644 --- a/src/Form/Type/OrderItemDiscountCollectionType.php +++ b/src/Form/Type/OrderItemDiscountCollectionType.php @@ -4,13 +4,40 @@ namespace Setono\SyliusOrderEditPlugin\Form\Type; +use Setono\SyliusOrderEditPlugin\Adder\DiscountAdjustmentsAdderInterface; use Setono\SyliusOrderEditPlugin\Model\AdjustmentTypes; +use Sylius\Component\Core\Model\OrderItemInterface; use Sylius\Component\Order\Factory\AdjustmentFactoryInterface; +use Sylius\Component\Order\Model\AdjustableInterface; +use Webmozart\Assert\Assert; final class OrderItemDiscountCollectionType extends CustomDiscountCollectionType { - public function __construct(AdjustmentFactoryInterface $adjustmentFactory) - { + public function __construct( + AdjustmentFactoryInterface $adjustmentFactory, + private readonly DiscountAdjustmentsAdderInterface $discountAdjustmentsAdder, + ) { parent::__construct($adjustmentFactory, 'Custom item discount', AdjustmentTypes::SETONO_ADMIN_ORDER_ITEM_DISCOUNT); } + + /** @psalm-ignore-var $adjustable */ + public function setDiscounts(AdjustableInterface $adjustable, array $discounts): void + { + Assert::isInstanceOf($adjustable, OrderItemInterface::class); + + $adjustable->removeAdjustmentsRecursively($this->adjustmentType); + /** @var int $orderItemId */ + $orderItemId = $adjustable->getId(); + + /** @var int $discount */ + foreach ($discounts as $discount) { + $this->discountAdjustmentsAdder->add( + $adjustable, + $this->adjustmentType, + $this->adjustmentType . '_' . $orderItemId, + 'Custom item discount', + -$discount, + ); + } + } } diff --git a/src/Resources/config/services/form.xml b/src/Resources/config/services/form.xml index 460feea..f43a7d8 100644 --- a/src/Resources/config/services/form.xml +++ b/src/Resources/config/services/form.xml @@ -3,12 +3,13 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> - + %sylius.model.order_item.class% %sylius.form.type.order_item.validation_groups% - @@ -17,6 +18,7 @@ class="Setono\SyliusOrderEditPlugin\Form\Type\OrderDiscountCollectionType" > + @@ -25,12 +27,15 @@ class="Setono\SyliusOrderEditPlugin\Form\Type\OrderItemDiscountCollectionType" > + - + diff --git a/src/Resources/config/services/order_processing.xml b/src/Resources/config/services/order_processing.xml index 14e501b..9c8a21d 100644 --- a/src/Resources/config/services/order_processing.xml +++ b/src/Resources/config/services/order_processing.xml @@ -57,5 +57,21 @@ + + + + + + + + + + diff --git a/src/Setter/OrderDiscountAdjustmentSetter.php b/src/Setter/OrderDiscountAdjustmentSetter.php new file mode 100644 index 0000000..d0f5396 --- /dev/null +++ b/src/Setter/OrderDiscountAdjustmentSetter.php @@ -0,0 +1,42 @@ +getItems(); + /** @var int $orderId */ + $orderId = $order->getId(); + + $distributedPrices = $this->integerDistributor->distribute($discount, $items->count()); + + /** @var int $distribution */ + foreach ($distributedPrices as $i => $distribution) { + /** @var OrderItemInterface $item */ + $item = $items->get($i); + $this->orderItemDiscountAdjustmentAdder->add( + $item, + AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT, + AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT . '_' . $orderId, + 'Custom order discount', + -$distribution, + ); + } + } +} diff --git a/src/Setter/OrderDiscountAdjustmentSetterInterface.php b/src/Setter/OrderDiscountAdjustmentSetterInterface.php new file mode 100644 index 0000000..579d372 --- /dev/null +++ b/src/Setter/OrderDiscountAdjustmentSetterInterface.php @@ -0,0 +1,12 @@ + - {{ money.format(item.units.first.adjustmentsTotal(unitPromotionAdjustment), order.currencyCode) }} + {{ money.format( + item.units.first.adjustmentsTotal(unitPromotionAdjustment) + item.units.first.adjustmentsTotal(adminOrderItemDiscountAdjustment), + order.currencyCode + ) }} - ~ {{ money.format(item.units.first.adjustmentsTotal(orderPromotionAdjustment), order.currencyCode) }} + ~ {{ money.format( + item.units.first.adjustmentsTotal(orderPromotionAdjustment) + item.units.first.adjustmentsTotal(adminOrderDiscountAdjustment), + order.currencyCode + ) }} - {{ money.format(item.fullDiscountedUnitPrice, order.currencyCode) }} + {{ money.format( + item.fullDiscountedUnitPrice + item.units.first.adjustmentsTotal(adminOrderItemDiscountAdjustment) + item.units.first.adjustmentsTotal(adminOrderDiscountAdjustment), + order.currencyCode + ) }} {{ item.quantity }} @@ -49,14 +59,3 @@ {{ money.format(item.total, order.currencyCode) }} -{% set discounts = item.getAdjustments(adminOrderItemDiscountAdjustment) %} -{% if discounts is not empty %} - - - {{ 'setono_sylius_order_edit.ui.discounts'|trans }}: - {% for discount in discounts %} - {{ money.format(discount.amount, order.currencyCode) }}{% if not loop.last %}, {% endif %} - {% endfor %} - - -{% endif %} diff --git a/tests/Application/templates/bundles/SyliusAdminBundle/Order/Show/Summary/_totalsPromotions.html.twig b/tests/Application/templates/bundles/SyliusAdminBundle/Order/Show/Summary/_totalsPromotions.html.twig index 46db361..969d063 100644 --- a/tests/Application/templates/bundles/SyliusAdminBundle/Order/Show/Summary/_totalsPromotions.html.twig +++ b/tests/Application/templates/bundles/SyliusAdminBundle/Order/Show/Summary/_totalsPromotions.html.twig @@ -3,12 +3,14 @@ {% set orderPromotionAdjustment = constant('Sylius\\Component\\Core\\Model\\AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT') %} {% set unitPromotionAdjustment = constant('Sylius\\Component\\Core\\Model\\AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT') %} {% set adminOrderDiscountAdjustment = constant('Setono\\SyliusOrderEditPlugin\\Model\\AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT') %} +{% set adminOrderItemDiscountAdjustment = constant('Setono\\SyliusOrderEditPlugin\\Model\\AdjustmentTypes::SETONO_ADMIN_ORDER_ITEM_DISCOUNT') %} {% set orderPromotionAdjustments = sylius_aggregate_adjustments(order.getAdjustmentsRecursively(orderPromotionAdjustment)) %} {% set unitPromotionAdjustments = sylius_aggregate_adjustments(order.getAdjustmentsRecursively(unitPromotionAdjustment)) %} {% set adminOrderDiscountAdjustments = sylius_aggregate_adjustments(order.getAdjustmentsRecursively(adminOrderDiscountAdjustment)) %} + {% set adminOrderItemDiscountAdjustments = sylius_aggregate_adjustments(order.getAdjustmentsRecursively(adminOrderItemDiscountAdjustment)) %} {% set promotionAdjustments = orderPromotionAdjustments|merge(unitPromotionAdjustments)|merge(adminOrderDiscountAdjustments) %} {% if not promotionAdjustments is empty %}
@@ -28,7 +30,8 @@ {% set orderPromotionTotal = order.getAdjustmentsTotalRecursively(orderPromotionAdjustment) %} {% set unitPromotionTotal = order.getAdjustmentsTotalRecursively(unitPromotionAdjustment) %} {% set adminOrderDiscountTotal = order.getAdjustmentsTotalRecursively(adminOrderDiscountAdjustment) %} + {% set adminOrderItemDiscountTotal = order.getAdjustmentsTotalRecursively(adminOrderItemDiscountAdjustment) %} {{ 'sylius.ui.promotion_total'|trans }}: - {{ money.format(orderPromotionTotal + unitPromotionTotal + adminOrderDiscountTotal, order.currencyCode) }} + {{ money.format(orderPromotionTotal + unitPromotionTotal + adminOrderDiscountTotal + adminOrderItemDiscountTotal, order.currencyCode) }} diff --git a/tests/Functional/OrderUpdateTest.php b/tests/Functional/OrderUpdateTest.php index d7fe5d2..e9ab8cb 100644 --- a/tests/Functional/OrderUpdateTest.php +++ b/tests/Functional/OrderUpdateTest.php @@ -116,7 +116,7 @@ public function testItAllowsToAddAndRemoveDiscountsForTheWholeOrderMultipleTimes /** @var EditableOrderInterface $order */ $order = $this->getOrderRepository()->findOneBy(['tokenValue' => 'TOKEN']); self::assertSame($initialOrderTotalWithoutTaxes - 200, $this->getResultTotal($order)); - self::assertSame(-200, $order->getAdjustmentsTotal(AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT)); + self::assertSame(-200, $order->getAdjustmentsTotalRecursively(AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT)); } public function testItDoesNotAllowToExceedTheInitialOrderTotal(): void @@ -157,10 +157,9 @@ public function testItAllowsToAddDiscountsForTheSpecificOrderItem(): void /** @var OrderInterface $order */ $order = $this->getOrderRepository()->findOneBy(['tokenValue' => 'TOKEN']); self::assertSame($initialOrderTotalWithoutTaxes - 100, $this->getResultTotal($order)); - self::assertSame(0, $order->getAdjustmentsTotal(AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT)); self::assertSame( -100, - $order->getItems()->first()->getAdjustmentsTotal(AdjustmentTypes::SETONO_ADMIN_ORDER_ITEM_DISCOUNT), + $order->getItems()->first()->getAdjustmentsTotalRecursively(AdjustmentTypes::SETONO_ADMIN_ORDER_ITEM_DISCOUNT), ); } @@ -183,10 +182,9 @@ public function testItAllowsToAddAndRemoveDiscountsForTheOrderItemMultipleTimes( /** @var EditableOrderInterface $order */ $order = $this->getOrderRepository()->findOneBy(['tokenValue' => 'TOKEN']); self::assertSame($initialOrderTotalWithoutTaxes - 200, $this->getResultTotal($order)); - self::assertSame(0, $order->getAdjustmentsTotal(AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT)); self::assertSame( -200, - $order->getItems()->first()->getAdjustmentsTotal(AdjustmentTypes::SETONO_ADMIN_ORDER_ITEM_DISCOUNT), + $order->getItems()->first()->getAdjustmentsTotalRecursively(AdjustmentTypes::SETONO_ADMIN_ORDER_ITEM_DISCOUNT), ); } diff --git a/tests/Unit/Adder/DiscountAdjustmentsAdderTest.php b/tests/Unit/Adder/DiscountAdjustmentsAdderTest.php new file mode 100644 index 0000000..a9c1ae8 --- /dev/null +++ b/tests/Unit/Adder/DiscountAdjustmentsAdderTest.php @@ -0,0 +1,67 @@ +prophesize(IntegerDistributorInterface::class); + $adjustmentFactory = $this->prophesize(AdjustmentFactoryInterface::class); + + $item = $this->prophesize(OrderItemInterface::class); + $firstUnit = $this->prophesize(OrderItemUnitInterface::class); + $secondUnit = $this->prophesize(OrderItemUnitInterface::class); + $thirdUnit = $this->prophesize(OrderItemUnitInterface::class); + + $adder = new DiscountAdjustmentsAdder( + $integerDistributor->reveal(), + $adjustmentFactory->reveal(), + ); + + $item->getQuantity()->willReturn(3); + $item->getUnits()->willReturn(new ArrayCollection( + [$firstUnit->reveal(), $secondUnit->reveal(), $thirdUnit->reveal()], + )); + + $integerDistributor->distribute(-1000, 3)->willReturn([-333, -333, -334]); + + $firstAdjustment = $this->prophesize(AdjustmentInterface::class); + $secondAdjustment = $this->prophesize(AdjustmentInterface::class); + $thirdAdjustment = $this->prophesize(AdjustmentInterface::class); + + $adjustmentFactory + ->createWithData(AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT, 'Label', -333) + ->willReturn($firstAdjustment->reveal(), $secondAdjustment->reveal()) + ; + $adjustmentFactory + ->createWithData(AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT, 'Label', -334, ) + ->willReturn($thirdAdjustment->reveal()) + ; + + $firstAdjustment->setOriginCode('ORIGIN_CODE')->shouldBeCalled(); + $secondAdjustment->setOriginCode('ORIGIN_CODE')->shouldBeCalled(); + $thirdAdjustment->setOriginCode('ORIGIN_CODE')->shouldBeCalled(); + + $firstUnit->addAdjustment($firstAdjustment->reveal())->shouldBeCalled(); + $secondUnit->addAdjustment($secondAdjustment->reveal())->shouldBeCalled(); + $thirdUnit->addAdjustment($thirdAdjustment->reveal())->shouldBeCalled(); + + $adder->add($item->reveal(), AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT, 'ORIGIN_CODE', 'Label', -1000); + } +} diff --git a/tests/Unit/Setter/OrderDiscountAdjustmentSetterTest.php b/tests/Unit/Setter/OrderDiscountAdjustmentSetterTest.php new file mode 100644 index 0000000..74deb92 --- /dev/null +++ b/tests/Unit/Setter/OrderDiscountAdjustmentSetterTest.php @@ -0,0 +1,54 @@ +prophesize(IntegerDistributorInterface::class); + $orderItemDiscountAdjustmentAdder = $this->prophesize(DiscountAdjustmentsAdderInterface::class); + + $setter = new OrderDiscountAdjustmentSetter( + $integerDistributor->reveal(), + $orderItemDiscountAdjustmentAdder->reveal(), + ); + + $order = $this->prophesize(EditableOrderInterface::class); + $firstItem = $this->prophesize(OrderItemInterface::class); + $secondItem = $this->prophesize(OrderItemInterface::class); + $order->getItems()->willReturn(new ArrayCollection([$firstItem->reveal(), $secondItem->reveal()])); + $order->getId()->willReturn(100); + + $integerDistributor + ->distribute(1000, 2) + ->willReturn([500, 500]) + ; + + $orderItemDiscountAdjustmentAdder + ->add($firstItem->reveal(), AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT, AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT . '_100', 'Custom order discount', -500) + ->shouldBeCalled() + ; + + $orderItemDiscountAdjustmentAdder + ->add($secondItem->reveal(), AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT, AdjustmentTypes::SETONO_ADMIN_ORDER_DISCOUNT . '_100', 'Custom order discount', -500) + ->shouldBeCalled() + ; + + $setter->set($order->reveal(), 1000); + } +}