How to add another implementation of UnitRefundInterface? ========================================================= .. note:: This cookbook describes customization of a feature available only with `Sylius/RefundPlugin `_ installed. Why would you add a new unit? ----------------------------- In the current implementation, there are 2 basic unit refunds in RefundPlugin: * OrderItemUnitRefund * ShipmentRefund But what if you want to refund something else, or refund the whole item instead of the item unit? Hopefully, you'll do it quite easily by creating a new implementation of `UnitRefundInterface` and a few tagged services that will make use of it. How to implement a new unit refund? ----------------------------------- Let's say that you don't operate on item units, but you want to refund entire items. You will need to create a new refund model that will implement `UnitRefundInterface`. To do so, a few things need to be done. Besides creating the `OrderItemRefund`, we will also need to create :doc:`a new refund type ` and make everything work together. **1. Create a new refund type:** This has been already described in the (How to add another type of refund?)[custom-type-of-refund] cookbook. The code for the new refund type is as follows: .. code-block:: php itemId; } public function total(): int { return $this->total; } public static function type(): RefundType { return RefundType::orderItem(); } } **3. Disable OrderItemUnitRefund:** As our new behavior bases on the order items, we need to disable the current behavior that is based on the order item units. We can achieve that by disabling the converters and the refunder by removing tags from the services: .. code-block:: yaml Sylius\RefundPlugin\Converter\OrderItemUnitLineItemsConverter: tags: [] Sylius\RefundPlugin\Converter\RequestToOrderItemUnitRefundConverter: tags: [] Sylius\RefundPlugin\Refunder\OrderItemUnitsRefunder: tags: [] **4. Create the OrderItemTotalProvider:** RefundPlugin doesn't know anything about order items, so we need to tell them how to retrieve the total of the order. .. code-block:: php orderItemRepository->find($id); Assert::notNull($orderItem); return $orderItem->getTotal(); } } As you can see, it just gets the order item and returns its total. Now a piece of configuration: .. code-block:: yaml App\Provider\OrderItemTotalProvider: arguments: - '@sylius.repository.order_item' tags: [{ name: 'sylius_refund.refund_unit_total_provider', refund_type: 'order_item' }] **5. Create the ItemRefunded event with a listener:** As we are refunding the order items, we need to update the payment state of the order. The event itself will be dispatched by the `OrderItemsRefunder` later. The `ItemRefunded` event is as follows: .. code-block:: php orderNumber; } } the listener: .. code-block:: php orderPartiallyRefundedStateResolver->resolve($itemRefunded->orderNumber()); } } and the configuration: .. code-block:: yaml App\Listener\ItemRefundedEventListener: arguments: - '@Sylius\RefundPlugin\StateResolver\OrderPartiallyRefundedStateResolverInterface' tags: [{ name: 'messenger.message_handler', bus: 'sylius.event_bus' }] **6. Create the OrderItemsRefunder:** Refunder will make use of previously created event by dispatching it at the end of refunding process. The refunding process basically processes the `OrderItemRefund` objects one by one to create a refund for each of them. .. code-block:: php unitRefundFilter->filterUnitRefunds($units, OrderItemRefund::class); $refundedTotal = 0; /** @var UnitRefundInterface $unit */ foreach ($units as $unit) { $this->refundCreator->__invoke( $orderNumber, $unit->id(), $unit->total(), $unit->type() ); $refundedTotal += $unit->total(); } $this->eventBus->dispatch(new ItemRefunded($orderNumber)); return $refundedTotal; } } Now add a tag to the service: .. code-block:: yaml App\Refunder\OrderItemsRefunder: arguments: - '@Sylius\RefundPlugin\Creator\RefundCreatorInterface' - '@sylius.event_bus' - '@Sylius\RefundPlugin\Filter\UnitRefundFilterInterface' tags: ['sylius_refund.refunder'] **7. Create the OrderItemLineItemsConverter:** RefundPlugin generates a credit memo based on a refund that was made. However, as it's handled under the hood by processing line items, we need to provide a converter that will convert the `OrderItemRefund` objects to the `LineItem` objects. .. code-block:: php getUnitRefundClass()); $lineItems = []; /** @var OrderItemRefund $unit */ foreach ($units as $unit) { $lineItems = $this->addLineItem($this->convertUnitRefundToLineItem($unit), $lineItems); } return $lineItems; } public function getUnitRefundClass(): string { return OrderItemRefund::class; } private function convertUnitRefundToLineItem(OrderItemRefund $unitRefund): LineItemInterface { /** @var OrderItemInterface|null $orderItem */ $orderItem = $this->orderItemRepository->find($unitRefund->id()); Assert::notNull($orderItem); Assert::lessThanEq($unitRefund->total(), $orderItem->getTotal()); $grossValue = $unitRefund->total(); $taxAmount = (int) ($grossValue * $orderItem->getTaxTotal() / $orderItem->getTotal()); $netValue = $grossValue - $taxAmount; /** @var string|null $productName */ $productName = $orderItem->getProductName(); Assert::notNull($productName); return new LineItem( $productName, 1, $netValue, $grossValue, $netValue, $grossValue, $taxAmount, $this->taxRateProvider->provide($orderItem) ); } /** * @param LineItemInterface[] $lineItems * * @return LineItemInterface[] */ private function addLineItem(LineItemInterface $newLineItem, array $lineItems): array { foreach ($lineItems as $lineItem) { if ($lineItem->compare($newLineItem)) { $lineItem->merge($newLineItem); return $lineItems; } } $lineItems[] = $newLineItem; return $lineItems; } } and the configuration: .. code-block:: yaml App\Converter\OrderItemLineItemsConverter: arguments: - '@sylius.repository.order_item' - '@Sylius\RefundPlugin\Provider\TaxRateProviderInterface' tags: ['sylius_refund.line_item_converter'] **8. Create the RequestToOrderItemRefundConverter:** Similar to the previous step, we need to provide a converter that will convert the request to the `OrderItemRefund` objects. .. code-block:: php refundUnitsConverter->convert( $request->request->all()['sylius_refund_items'] ?? [], OrderItemRefund::class ); } } and the configuration: .. code-block:: yaml App\Converter\RequestToOrderItemRefundConverter: arguments: - '@Sylius\RefundPlugin\Converter\RefundUnitsConverterInterface' tags: ['sylius_refund.request_to_refund_units_converter'] It's almost done! If you want to be able to refund the order items in the admin panel, one more step is needed. **9. Adjust the order refund form to the current state:** Under the `templates/Admin/OrderRefund` directory, create the `_items.html.twig` file. The template could look like this: .. code-block:: html+twig {% import '@SyliusAdmin/Common/Macro/money.html.twig' as money %} {% for item in order.items %} {% set variant = item.variant %} {% set product = variant.product %} {% include '@SyliusAdmin/Product/_info.html.twig' %} {{ money.format(item.total, order.currencyCode) }} {% set refundedTotal = unit_refunded_total(item.id, constant('App\\Entity\\Refund\\RefundType::ORDER_ITEM')) %} {% if refundedTotal != 0 %}
{{ 'sylius_refund.ui.refunded'|trans }}: {{ money.format(refundedTotal, order.currencyCode) }} {% endif %} {% set inputName = "sylius_refund_items["~item.id~"][amount]" %} {% set hiddenInputName = "sylius_refund_items["~item.id~"][partial-id]" %}
{{ order.currencyCode|sylius_currency_symbol }}
{% endfor %} The template above will be used in `sylius_refund.admin.order.refund.form.table.body` template event as e.g. `custom_items` block. Remember to disable `items` block, which handles order item units by the occasion. .. code-block:: yaml sylius_ui: events: sylius_refund.admin.order.refund.form.table.body: blocks: items: false custom_items: template: "Admin/OrderRefund/_items.html.twig" priority: 10 Great! Now you can refund the order items instead order item units in the admin panel.