Skip to content

Commit

Permalink
bug #296 Always create RefundPayment after CreditMemo (Zales0123)
Browse files Browse the repository at this point in the history
This PR was merged into the 1.0-dev branch.

Discussion
----------

Partially solves #91

With the current implementation, both **CreditMemo** and **RefundPayment** are generated separately, which can results in a situation when one of them exists and the other does not. With the proposed change, we're protected from such a situation.

For sure, the solution is not perfect. The whole compensation/rollback functionality is based on the [DoctrineTransactionMiddleware](https://github.com/symfony/doctrine-bridge/blob/5.4/Messenger/DoctrineTransactionMiddleware.php#L37), which works in this case but does not have to be enough for some more complicated processes. A possible improvement of the proposed architecture would be adding the `prev/compensate/rollback` function to the `UnitsRefundedProcessStepInterface` to implement a more mature and reliable saga pattern 🖖 

Commits
-------

261e0d3 Change current process manager to process steps
82f42a1 Describe desired post-refund behaviour with scenarios
7cf06b7 Tag services and document the whole process
90db258 PR review fixes
  • Loading branch information
GSadee authored May 26, 2021
2 parents ad02c0f + 90db258 commit 41fc088
Show file tree
Hide file tree
Showing 20 changed files with 366 additions and 26 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,23 @@ as shown below:
Online refund logic should be implemented if you need it.
As the first try for the possible customization, you can check out `Sylius\RefundPlugin\Event\UnitsRefunded` event.
## Post-refunding process
After units are refunded, there are multiple other processes that should be triggered. By default, after units refund, there should be **CreditMemo** and
**RefundPayment** generated. As they're strictly coupled with each other, **RefundPayment** is always created after the **CreditMemo**. Moreover, if **RefundPayment**
fails, related **CreditMemo** should not be created as well.

`Sylius\RefundPlugin\ProcessManager\UnitsRefundedProcessManager` service facilitates the whole process. If you want to add one or more steps to it, you should create
a service implementing `Sylius\RefundPlugin\ProcessManager\UnitsRefundedProcessStepInterface`, and register if with proper tag:

```yaml
App\ProcessManager\CustomAfterRefundProcessManager:
tags:
- { name: sylius_refund.units_refunded.process_step, priority: 0 }
```
Tagged services would be executed according to their priority (descending).
## Security issues
If you think that you have found a security issue, please do not use the issue tracker and do not post it publicly.
Expand Down
7 changes: 7 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ is created by `Sylius\RefundPlugin\Factory\CreditMemoFactory`.
}
```

1. Post-units refunded process (containing credit memo and refund payment generation) was changed to synchronous step. Refund payment is therefore
always generated after credit memo. Technical changes:
* `Sylius\RefundPlugin\ProcessManager\CreditMemoProcessManager` and `Sylius\RefundPlugin\ProcessManager\RefundPaymentProcessManager` no longer
directly listen to `Sylius\RefundPlugin\Event\UnitsRefunded` event. `Sylius\RefundPlugin\ProcessManager\UnitsRefundedProcessManager` uses them to
facilitate post-units refunding process.
* Their `__invoke` methods were replaced by `Sylius\RefundPlugin\ProcessManager\UnitsRefundedProcessStepInterface::next(UnitsRefunded $unitsRefunded)`.

### UPGRADE FROM 1.0.0-RC.7 TO 1.0.0-RC.8

1. The `fully_refunded` state and the `refund` transition have been removed from `sylius_order` state machine.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@refunds
Feature: Not seeing refund payments and credit memos on admin order view
In order to have consistent documents and payment in a refunded order
As an Administrator
I don't want to have a credit memo or refund payment generated alone

Background:
Given the store operates on a single green channel in "United States"
And the store has a product "Mr. Meeseeks T-Shirt" priced at "$10.00"
And the store has a product "Angel T-Shirt" priced at "$5.00"
And the store allows shipping with "Galaxy Post"
And the store allows paying with "Space money"
And there is a customer "[email protected]" that placed an order "#00000023"
And the customer bought 2 "Mr. Meeseeks T-Shirt" products
And the customer chose "Galaxy Post" shipping method to "United States" with "Space money" payment
And the order "#00000023" is already paid
And I am logged in as an administrator

@ui
Scenario: Not seeing credit memo and refund payment on order view if credit memo generation failed
Given the credit memo generation is broken
And I decided to refund 1st "Mr. Meeseeks T-Shirt" product of the order "00000023" with "Space money" payment
When I view the summary of the order "#00000023"
Then I should not see any refund payments
And I should not see any credit memos

@ui
Scenario: Not seeing credit memo and refund payment on order view if refund generation failed
Given the refund payment generation is broken
And I decided to refund 1st "Mr. Meeseeks T-Shirt" product of the order "00000023" with "Space money" payment
When I view the summary of the order "#00000023"
Then I should not see any refund payments
And I should not see any credit memos
8 changes: 7 additions & 1 deletion spec/ProcessManager/CreditMemoProcessManagerSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Sylius\RefundPlugin\Event\UnitsRefunded;
use Sylius\RefundPlugin\Model\OrderItemUnitRefund;
use Sylius\RefundPlugin\Model\ShipmentRefund;
use Sylius\RefundPlugin\ProcessManager\UnitsRefundedProcessStepInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;

Expand All @@ -19,6 +20,11 @@ function let(MessageBusInterface $commandBus): void
$this->beConstructedWith($commandBus);
}

function it_implements_units_refunded_process_step_interface(): void
{
$this->shouldImplement(UnitsRefundedProcessStepInterface::class);
}

function it_reacts_on_units_generated_event_and_dispatch_generate_credit_memo_command(MessageBusInterface $commandBus)
{
$unitRefunds = [new OrderItemUnitRefund(1, 1000), new OrderItemUnitRefund(3, 2000), new OrderItemUnitRefund(5, 3000)];
Expand All @@ -27,6 +33,6 @@ function it_reacts_on_units_generated_event_and_dispatch_generate_credit_memo_co
$command = new GenerateCreditMemo('000222', 3000, $unitRefunds, $shipmentRefunds, 'Comment');
$commandBus->dispatch($command)->willReturn(new Envelope($command))->shouldBeCalled();

$this(new UnitsRefunded('000222', $unitRefunds, $shipmentRefunds, 1, 3000, 'USD', 'Comment'));
$this->next(new UnitsRefunded('000222', $unitRefunds, $shipmentRefunds, 1, 3000, 'USD', 'Comment'));
}
}
16 changes: 15 additions & 1 deletion spec/ProcessManager/RefundPaymentProcessManagerSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Sylius\RefundPlugin\Factory\RefundPaymentFactoryInterface;
use Sylius\RefundPlugin\Model\OrderItemUnitRefund;
use Sylius\RefundPlugin\Model\ShipmentRefund;
use Sylius\RefundPlugin\ProcessManager\UnitsRefundedProcessStepInterface;
use Sylius\RefundPlugin\Provider\RelatedPaymentIdProviderInterface;
use Sylius\RefundPlugin\StateResolver\OrderFullyRefundedStateResolverInterface;
use Symfony\Component\Messenger\Envelope;
Expand All @@ -35,6 +36,11 @@ function let(
);
}

function it_implements_units_refunded_process_step_interface(): void
{
$this->shouldImplement(UnitsRefundedProcessStepInterface::class);
}

function it_reacts_on_units_refunded_event_and_creates_refund_payment(
OrderFullyRefundedStateResolverInterface $orderFullyRefundedStateResolver,
RelatedPaymentIdProviderInterface $relatedPaymentIdProvider,
Expand Down Expand Up @@ -65,6 +71,14 @@ function it_reacts_on_units_refunded_event_and_creates_refund_payment(
$event = new RefundPaymentGenerated(10, '000222', 1000, 'USD', 1, 3);
$eventBus->dispatch($event)->willReturn(new Envelope($event))->shouldBeCalled();

$this(new UnitsRefunded('000222', [new OrderItemUnitRefund(1, 500), new OrderItemUnitRefund(2, 500)], [new ShipmentRefund(1, 300)], 1, 1000, 'USD', 'Comment'));
$this->next(new UnitsRefunded(
'000222',
[new OrderItemUnitRefund(1, 500), new OrderItemUnitRefund(2, 500)],
[new ShipmentRefund(1, 300)],
1,
1000,
'USD',
'Comment'
));
}
}
41 changes: 41 additions & 0 deletions spec/ProcessManager/UnitsRefundedProcessManagerSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace spec\Sylius\RefundPlugin\ProcessManager;

use PhpSpec\ObjectBehavior;
use Sylius\RefundPlugin\Event\UnitsRefunded;
use Sylius\RefundPlugin\Model\OrderItemUnitRefund;
use Sylius\RefundPlugin\Model\ShipmentRefund;
use Sylius\RefundPlugin\ProcessManager\UnitsRefundedProcessManagerInterface;
use Sylius\RefundPlugin\ProcessManager\UnitsRefundedProcessStepInterface;

final class UnitsRefundedProcessManagerSpec extends ObjectBehavior
{
function let(
UnitsRefundedProcessStepInterface $creditMemoProcessManager,
UnitsRefundedProcessStepInterface $refundPaymentProcessManager
): void {
$this->beConstructedWith([$creditMemoProcessManager, $refundPaymentProcessManager]);
}

function it_implements_units_refunded_process_manager_interface(): void
{
$this->shouldImplement(UnitsRefundedProcessManagerInterface::class);
}

function it_triggers_all_process_steps_if_all_are_successful(
UnitsRefundedProcessStepInterface $creditMemoProcessManager,
UnitsRefundedProcessStepInterface $refundPaymentProcessManager
): void {
$unitRefunds = [new OrderItemUnitRefund(1, 1000), new OrderItemUnitRefund(3, 2000), new OrderItemUnitRefund(5, 3000)];
$shipmentRefunds = [new ShipmentRefund(1, 500), new ShipmentRefund(2, 1000)];
$event = new UnitsRefunded('000222', $unitRefunds, $shipmentRefunds, 1, 1500, 'USD', 'Comment');

$creditMemoProcessManager->next($event)->shouldBeCalled();
$refundPaymentProcessManager->next($event)->shouldBeCalled();

$this($event);
}
}
14 changes: 7 additions & 7 deletions src/ProcessManager/CreditMemoProcessManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Sylius\RefundPlugin\Event\UnitsRefunded;
use Symfony\Component\Messenger\MessageBusInterface;

final class CreditMemoProcessManager
final class CreditMemoProcessManager implements UnitsRefundedProcessStepInterface
{
/** @var MessageBusInterface */
private $commandBus;
Expand All @@ -18,14 +18,14 @@ public function __construct(MessageBusInterface $commandBus)
$this->commandBus = $commandBus;
}

public function __invoke(UnitsRefunded $event): void
public function next(UnitsRefunded $unitsRefunded): void
{
$this->commandBus->dispatch(new GenerateCreditMemo(
$event->orderNumber(),
$event->amount(),
$event->units(),
$event->shipments(),
$event->comment()
$unitsRefunded->orderNumber(),
$unitsRefunded->amount(),
$unitsRefunded->units(),
$unitsRefunded->shipments(),
$unitsRefunded->comment()
));
}
}
22 changes: 11 additions & 11 deletions src/ProcessManager/RefundPaymentProcessManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
use Sylius\RefundPlugin\StateResolver\OrderFullyRefundedStateResolverInterface;
use Symfony\Component\Messenger\MessageBusInterface;

final class RefundPaymentProcessManager
final class RefundPaymentProcessManager implements UnitsRefundedProcessStepInterface
{
/** @var OrderFullyRefundedStateResolverInterface */
private $orderFullyRefundedStateResolver;
Expand Down Expand Up @@ -44,28 +44,28 @@ public function __construct(
$this->eventBus = $eventBus;
}

public function __invoke(UnitsRefunded $event): void
public function next(UnitsRefunded $unitsRefunded): void
{
$refundPayment = $this->refundPaymentFactory->createWithData(
$event->orderNumber(),
$event->amount(),
$event->currencyCode(),
$unitsRefunded->orderNumber(),
$unitsRefunded->amount(),
$unitsRefunded->currencyCode(),
RefundPaymentInterface::STATE_NEW,
$event->paymentMethodId()
$unitsRefunded->paymentMethodId()
);

$this->entityManager->persist($refundPayment);
$this->entityManager->flush();

$this->eventBus->dispatch(new RefundPaymentGenerated(
$refundPayment->getId(),
$event->orderNumber(),
$event->amount(),
$event->currencyCode(),
$event->paymentMethodId(),
$unitsRefunded->orderNumber(),
$unitsRefunded->amount(),
$unitsRefunded->currencyCode(),
$unitsRefunded->paymentMethodId(),
$this->relatedPaymentIdProvider->getForRefundPayment($refundPayment)
));

$this->orderFullyRefundedStateResolver->resolve($event->orderNumber());
$this->orderFullyRefundedStateResolver->resolve($unitsRefunded->orderNumber());
}
}
26 changes: 26 additions & 0 deletions src/ProcessManager/UnitsRefundedProcessManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Sylius\RefundPlugin\ProcessManager;

use Sylius\RefundPlugin\Event\UnitsRefunded;

final class UnitsRefundedProcessManager implements UnitsRefundedProcessManagerInterface
{
/** @var iterable|UnitsRefundedProcessStepInterface[] */
private $steps;

public function __construct(iterable $steps)
{
$this->steps = $steps;
}

public function __invoke(UnitsRefunded $event): void
{
/** @var UnitsRefundedProcessStepInterface $step */
foreach ($this->steps as $step) {
$step->next($event);
}
}
}
12 changes: 12 additions & 0 deletions src/ProcessManager/UnitsRefundedProcessManagerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Sylius\RefundPlugin\ProcessManager;

use Sylius\RefundPlugin\Event\UnitsRefunded;

interface UnitsRefundedProcessManagerInterface
{
public function __invoke(UnitsRefunded $event): void;
}
12 changes: 12 additions & 0 deletions src/ProcessManager/UnitsRefundedProcessStepInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Sylius\RefundPlugin\ProcessManager;

use Sylius\RefundPlugin\Event\UnitsRefunded;

interface UnitsRefundedProcessStepInterface
{
public function next(UnitsRefunded $unitsRefunded): void;
}
14 changes: 11 additions & 3 deletions src/Resources/config/services/event_bus.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@
<tag name="messenger.message_handler" bus="sylius.event_bus" />
</service>

<service id="Sylius\RefundPlugin\ProcessManager\CreditMemoProcessManager">
<argument type="service" id="sylius.command_bus" />
<service
id="Sylius\RefundPlugin\ProcessManager\UnitsRefundedProcessManagerInterface"
class="Sylius\RefundPlugin\ProcessManager\UnitsRefundedProcessManager"
>
<argument type="tagged_iterator" tag="sylius_refund.units_refunded.process_step"/>
<tag name="messenger.message_handler" bus="sylius.event_bus" />
</service>

Expand All @@ -30,7 +33,12 @@
<argument type="service" id="Sylius\RefundPlugin\Factory\RefundPaymentFactory" />
<argument type="service" id="doctrine.orm.default_entity_manager" />
<argument type="service" id="sylius.event_bus" />
<tag name="messenger.message_handler" bus="sylius.event_bus" />
<tag name="sylius_refund.units_refunded.process_step" priority="50" />
</service>

<service id="Sylius\RefundPlugin\ProcessManager\CreditMemoProcessManager">
<argument type="service" id="sylius.command_bus" />
<tag name="sylius_refund.units_refunded.process_step" priority="100" />
</service>
</services>
</container>
11 changes: 11 additions & 0 deletions tests/Application/config/services_test.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
imports:
- { resource: "../../../vendor/sylius/sylius/src/Sylius/Behat/Resources/config/services.xml" }
- { resource: "../../Behat/Resources/services.xml" }

services:
Tests\Sylius\RefundPlugin\Behat\Services\Generator\FailedCreditMemoGenerator:
decorates: 'Sylius\RefundPlugin\Generator\CreditMemoGenerator'
arguments:
- '@Tests\Sylius\RefundPlugin\Behat\Services\Generator\FailedCreditMemoGenerator.inner'

Tests\Sylius\RefundPlugin\Behat\Services\Factory\FailedRefundPaymentFactory:
decorates: 'Sylius\RefundPlugin\Factory\RefundPaymentFactory'
arguments:
- '@Tests\Sylius\RefundPlugin\Behat\Services\Factory\FailedRefundPaymentFactory.inner'
Loading

0 comments on commit 41fc088

Please sign in to comment.