Skip to content

Commit

Permalink
Making the customer option calculation extendable with events (#103)
Browse files Browse the repository at this point in the history
* feat: added event dispatcher to CustomerOptionRecalculator

* refactor: optimized CustomerOptionRecalculator Events

* refactor: added a subscriber to create adjustment for select options

* refactor: replaced genericEvent with custom events

* chore: updated readme

* Stripping down the implementation

* Fixing the tests

Co-authored-by: Basil Baumgartner <basil.baumgartner@ongoing.ch>
  • Loading branch information
mamazu and seizan8 authored Oct 22, 2021
1 parent eda72a5 commit 347ad32
Show file tree
Hide file tree
Showing 10 changed files with 364 additions and 37 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class OrderItem extends BaseOrderItem implements OrderItemInterface
bin/console sylius:fixtures:load
```

* Finally update the database and update the translations:
* Finally, update the database and update the translations:
```bash
bin/console doctrine:migrations:migrate
bin/console translation:update
Expand Down
165 changes: 165 additions & 0 deletions docs/custom_adjustments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
## Customizing Order Adjustment Calculations

`Brille24\SyliusCustomerOptionsPlugin\Services\CustomerOptionRecalculator` handles all the adjustments for the customer
options. You can add custom adjustment simply by creating a subscriber and listening to one of the
`CustomerOptionRecalculator` events.

### Events

The following events are dispatched in this order.

| Event class | description |
| ------------------------------------- | ----------------------------------------------------------------------------------- |
| `RemoveCustomerOptionFromOrderEvent` | This gets called to give the opportunity to remove more customer option adjustments |
| `RecalculateOrderItemOptionEvent` | Gets called for every order item option that was added for the order |

### Creating a custom adjustment

Let's say our products have a text field for a gift card. The gift card costs 1 cent every 10 characters. First you have
to add a `CustomerOption` with the code "gift_card" and type "text". After you added the option to your products you can
create a subscriber to generate adjustments when customers insert a text for the gift card.

```php
<?php
declare(strict_types=1);

namespace App\Module\Brille24CustomerOptionsPlugin\Subscriber;

use Brille24\SyliusCustomerOptionsPlugin\Event\RecalculateOrderItemOptionEvent;
use Brille24\SyliusCustomerOptionsPlugin\Services\CustomerOptionRecalculator;
use Sylius\Component\Order\Factory\AdjustmentFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class GiftCardAdjustmentSubscriber implements EventSubscriberInterface
{
/** @var AdjustmentFactoryInterface */
private $adjustmentFactory;

public function __construct(
AdjustmentFactoryInterface $adjustmentFactory
) {
$this->adjustmentFactory = $adjustmentFactory;
}

public static function getSubscribedEvents(): array
{
return [
RecalculateOrderItemOptionEvent::class => 'createAdjustment',
];
}

public function createAdjustment(
RecalculateOrderItemOptionEvent $event
) {
$orderItemOption = $event->getOrderItemOption();
// Skip handling all other customer options
if ($orderItemOption->getCustomerOptionCode() !== 'gift_card') {
return;
}

$orderItem = $orderItemOption->getOrderItem();

$textValue = trim($orderItemOption->getOptionValue());
$costForTextOnCard = (int) ceil(mb_strlen($textValue) / 10);

if (mb_strlen($textValue) > 0) {
foreach ($orderItem->getUnits() as $unit) {
$adjustment = $this->adjustmentFactory->createWithData(
CustomerOptionRecalculator::CUSTOMER_OPTION_ADJUSTMENT,
'Gift card',
$costForTextOnCard,
false,
[]
);

$unit->addAdjustment($adjustment);
}
}
}
}

```

### Remove your custom adjustment

If you used the `CustomerOptionRecalculator::CUSTOMER_OPTION_ADJUSTMENT` constant for your adjustments you are done! All
adjustments of that type will be removed when the `CustomerOptionRecalculator` is executed. If you want to use your own
type for the adjustments you can use the `RemoveCustomerOptionFromOrderEvent` to remove your adjustments.

```php
<?php
declare(strict_types=1);

namespace App\Module\Brille24CustomerOptionsPlugin\Subscriber;

use Brille24\SyliusCustomerOptionsPlugin\Event\RemoveCustomerOptionFromOrderEvent;
use Brille24\SyliusCustomerOptionsPlugin\Event\RecalculateOrderItemOptionEvent;
use Brille24\SyliusCustomerOptionsPlugin\Services\CustomerOptionRecalculator;
use Sylius\Component\Order\Factory\AdjustmentFactoryInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class GiftCardAdjustmentSubscriber implements EventSubscriberInterface
{
const GIFT_CARD_ADJUSTMENT_TYPE = 'gift_card';

/** @var AdjustmentFactoryInterface */
private $adjustmentFactory;

/**
* @param AdjustmentFactoryInterface $adjustmentFactory
*/
public function __construct(
AdjustmentFactoryInterface $adjustmentFactory
) {
$this->adjustmentFactory = $adjustmentFactory;
}

public static function getSubscribedEvents()
{
return [
RemoveCustomerOptionFromOrderEvent::class => 'removeAdjustment',
RecalculateOrderItemOptionEvent::class => 'createAdjustment',
];
}

/**
* @param RemoveCustomerOptionFromOrderEvent $event
*/
public function removeAdjustment(RemoveCustomerOptionFromOrderEvent $event)
{
$order = $event->getOrder();
$order->removeAdjustmentsRecursively(self::GIFT_CARD_ADJUSTMENT_TYPE);
}

/**
* @param RecalculateOrderItemOptionEvent $event
*/
public function createAdjustment(RecalculateOrderItemOptionEvent $event)
{
$orderItemOption = $event->getOrderItemOption();
// Skip handling all other customer options
if ($orderItemOption->getCustomerOptionCode() !== 'gift_card') {
return;
}

$orderItem = $orderItemOption->getOrderItem();

$textValue = trim($orderItemOption->getOptionValue());
$costForTextOnCard = (int) ceil(mb_strlen($textValue) / 10);

if (mb_strlen($textValue) > 0) {
foreach ($orderItem->getUnits() as $unit) {
$adjustment = $this->adjustmentFactory->createWithData(
self::GIFT_CARD_ADJUSTMENT_TYPE,
'Gift card',
$costForTextOnCard,
false,
[]
);

$unit->addAdjustment($adjustment);
}
}
}
}
```
3 changes: 2 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ There are two cases that we want to describe. In the first case of [Making a pro
of creating everything from scratch. In [Generating Customer Options with fixtures](fixtures.md) we use fixtures to generate
random Customer Options.

**To see images of how it looks look into the `docs/images` folder.**
**To see images of how it looks, look into the `docs/images` folder.**

### Contents

- [Making a product customizable](making_a_product_customizable.md)
- [Generating Customer Options with fixtures](fixtures.md)
- [Conditional Constraints](conditional_constraints.md)
- [Customer option price overrides](price_override.md)
- [Customizing Order Adjustment Calculations](custom_adjustments.md)
- [Price import](price_import.md)
32 changes: 32 additions & 0 deletions src/Event/RecalculateOrderItemOptionEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/**
* This file is part of the Brille24 customer options plugin.
*
* (c) Brille24 GmbH
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);

namespace Brille24\SyliusCustomerOptionsPlugin\Event;

use Brille24\SyliusCustomerOptionsPlugin\Entity\OrderItemOptionInterface;
use Symfony\Contracts\EventDispatcher\Event;

class RecalculateOrderItemOptionEvent extends Event
{
/** @var OrderItemOptionInterface */
private $orderItemOption;

public function __construct(OrderItemOptionInterface $orderItemOption)
{
$this->orderItemOption = $orderItemOption;
}

public function getOrderItemOption(): OrderItemOptionInterface
{
return $this->orderItemOption;
}
}
32 changes: 32 additions & 0 deletions src/Event/RemoveCustomerOptionFromOrderEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/**
* This file is part of the Brille24 customer options plugin.
*
* (c) Brille24 GmbH
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);

namespace Brille24\SyliusCustomerOptionsPlugin\Event;

use Sylius\Component\Order\Model\OrderInterface;
use Symfony\Contracts\EventDispatcher\Event;

class RemoveCustomerOptionFromOrderEvent extends Event
{
/** @var OrderInterface */
private $order;

public function __construct(OrderInterface $order)
{
$this->order = $order;
}

public function getOrder(): OrderInterface
{
return $this->order;
}
}
2 changes: 1 addition & 1 deletion src/Resources/config/app/services/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<service class="Brille24\SyliusCustomerOptionsPlugin\Services\CustomerOptionRecalculator"
id="brille24.customer_options_plugin.services.order_prices_recalculator">
<argument type="service" id="sylius.custom_factory.adjustment" />
<argument type="service" id="event_dispatcher" />

<tag name="sylius.order_processor" priority="40" />
</service>
Expand Down
11 changes: 11 additions & 0 deletions src/Resources/config/app/services/subscribers.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<container xmlns="http://symfony.com/schema/dic/services">
<services>
<service
class="Brille24\SyliusCustomerOptionsPlugin\Subscriber\SelectAdjustmentCalculatorSubscriber"
id="brille24.customer_options_plugin.subscriber.adjustment"
>
<argument type="service" id="sylius.custom_factory.adjustment" />
<tag name="kernel.event_subscriber" />
</service>
</services>
</container>
39 changes: 12 additions & 27 deletions src/Services/CustomerOptionRecalculator.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,39 @@

use Brille24\SyliusCustomerOptionsPlugin\Entity\OrderItemInterface;
use Brille24\SyliusCustomerOptionsPlugin\Entity\OrderItemOptionInterface;
use Sylius\Component\Order\Factory\AdjustmentFactoryInterface;
use Brille24\SyliusCustomerOptionsPlugin\Event\RecalculateOrderItemOptionEvent;
use Brille24\SyliusCustomerOptionsPlugin\Event\RemoveCustomerOptionFromOrderEvent;
use Sylius\Component\Order\Model\OrderInterface;
use Sylius\Component\Order\Processor\OrderProcessorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

final class CustomerOptionRecalculator implements OrderProcessorInterface
{
public const CUSTOMER_OPTION_ADJUSTMENT = 'customer_option';

/** @var AdjustmentFactoryInterface */
private $adjustmentFactory;
/** @var EventDispatcherInterface */
private $eventDispatcher;

public function __construct(AdjustmentFactoryInterface $adjustmentFactory)
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->adjustmentFactory = $adjustmentFactory;
$this->eventDispatcher = $eventDispatcher;
}

public function process(OrderInterface $order): void
{
$order->removeAdjustmentsRecursively(self::CUSTOMER_OPTION_ADJUSTMENT);
$this->eventDispatcher->dispatch(new RemoveCustomerOptionFromOrderEvent($order));

foreach ($order->getItems() as $orderItem) {
if (!$orderItem instanceof OrderItemInterface) {
continue;
}

$this->addOrderItemAdjustment($orderItem);
}
}

private function addOrderItemAdjustment(OrderItemInterface $orderItem): void
{
/** @var OrderItemOptionInterface[] $configuration */
$configuration = $orderItem->getCustomerOptionConfiguration();
foreach ($configuration as $orderItemOption) {
// Skip all customer options that don't have customer option values as they can not have a price like
// text options
if (null === $orderItemOption->getCustomerOptionValue()) {
continue;
}

foreach ($orderItem->getUnits() as $unit) {
$adjustment = $this->adjustmentFactory->createWithData(
self::CUSTOMER_OPTION_ADJUSTMENT,
$orderItemOption->getCustomerOptionName(),
$orderItemOption->getCalculatedPrice($orderItem->getUnitPrice())
);
/** @var OrderItemOptionInterface[] $configuration */
$configuration = $orderItem->getCustomerOptionConfiguration();

$unit->addAdjustment($adjustment);
foreach ($configuration as $orderItemOption) {
$this->eventDispatcher->dispatch(new RecalculateOrderItemOptionEvent($orderItemOption));
}
}
}
Expand Down
Loading

0 comments on commit 347ad32

Please sign in to comment.