diff --git a/features/accept-order.feature b/features/accept-order.feature index c43c0ed..20aa58f 100644 --- a/features/accept-order.feature +++ b/features/accept-order.feature @@ -3,8 +3,9 @@ Feature: Background: Given there exist following clients: - | clientId | name | initialBalance | currentBalance | - | 018edd2e-894a-78d7-b10c-16e05ca933a3 | Kacper | 10000 | 200000 | + | clientId | name | initialBalance | currentBalance | locked | + | 018edd2e-894a-78d7-b10c-16e05ca933a3 | Kacper | 10000 | 200000 | no | + | 018ede1e-a587-76a8-88f3-23987a8dade9 | Jacek | 10000 | 60000000 | yes | Scenario: Frontend is able to send new orders Given the request has the following body: @@ -52,3 +53,21 @@ Feature: """ When I send a POST request to "/orders" Then the response status should be 422 + + Scenario: Locked clients should not be allowed + Given the request has the following body: + """ + { + "orderId": "018eddae-ff52-7813-9f88-ada9e61a76f3", + "clientId": "018ede1e-a587-76a8-88f3-23987a8dade9", + "products": [ + { "productId": "p1", "quantity": 2, "price": 20.0, "weight": 100 }, + { "productId": "p2", "quantity": 1, "price": 100.0, "weight": 200 }, + { "productId": "p3", "quantity": 5, "price": 200.0, "weight": 100 }, + { "productId": "p4", "quantity": 1, "price": 50.0, "weight": 400 }, + { "productId": "p5", "quantity": 10, "price": 10.0, "weight": 100 } + ] + } + """ + When I send a POST request to "/orders" + Then the response status should be 403 diff --git a/features/crm-user-sync.feature b/features/crm-user-sync.feature index 8003d09..3147f18 100644 --- a/features/crm-user-sync.feature +++ b/features/crm-user-sync.feature @@ -3,8 +3,9 @@ Feature: Background: Given there exist following clients: - | clientId | name | initialBalance | currentBalance | - | 018edd2e-894a-78d7-b10c-16e05ca933a3 | Kacper | 10000 | 5000 | + | clientId | name | initialBalance | currentBalance | locked | + | 018edd2e-894a-78d7-b10c-16e05ca933a3 | Kacper | 10000 | 5000 | no | + | 018ede1e-a587-76a8-88f3-23987a8dade9 | Jacek | 10000 | 60000000 | yes | Scenario: CRM is able to create new clients Given the request has the following body: @@ -43,3 +44,25 @@ Feature: When I send a POST request to "/clients" Then the response status should be 409 And client with id "018edd2e-894a-78d7-b10c-16e05ca933a3" should exist + + Scenario: CRM is able to lock existing client + Given the request has the following body: + """ + { + "lock": true + } + """ + When I send a POST request to "/clients/018edd2e-894a-78d7-b10c-16e05ca933a3/_lock" + Then the response status should be 200 + And client with id "018edd2e-894a-78d7-b10c-16e05ca933a3" should be locked + + Scenario: CRM is able to unlock existing client + Given the request has the following body: + """ + { + "lock": false + } + """ + When I send a POST request to "/clients/018ede1e-a587-76a8-88f3-23987a8dade9/_lock" + Then the response status should be 200 + And client with id "018ede1e-a587-76a8-88f3-23987a8dade9" should not be locked diff --git a/src/Contract/LockClientRequestDto.php b/src/Contract/LockClientRequestDto.php new file mode 100644 index 0000000..2b52ac0 --- /dev/null +++ b/src/Contract/LockClientRequestDto.php @@ -0,0 +1,8 @@ +<?php + +namespace App\Contract; + +readonly class LockClientRequestDto +{ + public function __construct(public bool $lock) {} +} diff --git a/src/Controller/ClientController.php b/src/Controller/ClientController.php index 1291a97..547862d 100644 --- a/src/Controller/ClientController.php +++ b/src/Controller/ClientController.php @@ -3,6 +3,7 @@ namespace App\Controller; use App\Contract\ClientDto; +use App\Contract\LockClientRequestDto; use App\Contract\TopUpRequestDto; use App\Entity\Client; use App\Exception\ClientAlreadyExistsException; @@ -49,4 +50,18 @@ readonly class ClientController return new Response(); } + + #[Route(path: '/{client}/_lock', name: 'lock', methods: ['POST'])] + public function lock( + #[MapRequestPayload] LockClientRequestDto $lockClientRequestDto, + Client $client, + ): Response { + if ($lockClientRequestDto->lock) { + $this->clientService->lock($client); + } else { + $this->clientService->unlock($client); + } + + return new Response(); + } } diff --git a/src/Controller/OrderController.php b/src/Controller/OrderController.php index 34699c2..d0f77b9 100644 --- a/src/Controller/OrderController.php +++ b/src/Controller/OrderController.php @@ -6,6 +6,7 @@ namespace App\Controller; use App\Aggregate\AcceptOrderCommand; use App\Contract\Order; +use App\Exception\ClientLockedException; use App\Exception\ClientNotExistsException; use App\Service\ClientService; use App\Service\OrderService; @@ -39,6 +40,8 @@ readonly class OrderController return new Response(); } catch (ClientNotExistsException) { return new Response(status: Response::HTTP_UNPROCESSABLE_ENTITY); + } catch (ClientLockedException) { + return new Response(status: Response::HTTP_FORBIDDEN); } } diff --git a/src/Entity/Client.php b/src/Entity/Client.php index ed15926..7f3a8d3 100644 --- a/src/Entity/Client.php +++ b/src/Entity/Client.php @@ -23,6 +23,9 @@ class Client #[ORM\Column] private ?int $currentBalance = null, + + #[ORM\Column(nullable: true)] + private ?\DateTimeImmutable $lockedAt = null, ) { $this->currentBalance ??= $this->initialBalance; } @@ -66,4 +69,19 @@ class Client { $this->currentBalance += $amount; } + + public function setLockedAt(?\DateTimeImmutable $dateTime) + { + $this->lockedAt = $dateTime; + } + + public function getLockedAt(): ?\DateTimeImmutable + { + return $this->lockedAt; + } + + public function isLocked(): bool + { + return $this->lockedAt !== null; + } } diff --git a/src/Exception/ClientLockedException.php b/src/Exception/ClientLockedException.php new file mode 100644 index 0000000..9477ca3 --- /dev/null +++ b/src/Exception/ClientLockedException.php @@ -0,0 +1,7 @@ +<?php + +namespace App\Exception; + +class ClientLockedException extends \LogicException +{ +} diff --git a/src/Service/ClientService.php b/src/Service/ClientService.php index 82bebe2..5103881 100644 --- a/src/Service/ClientService.php +++ b/src/Service/ClientService.php @@ -6,12 +6,14 @@ use App\Entity\Client; use App\Exception\ClientAlreadyExistsException; use App\Exception\ClientNotExistsException; use App\Repository\ClientRepository; +use Psr\Clock\ClockInterface; use Symfony\Component\Uid\Uuid; class ClientService { public function __construct( - public readonly ClientRepository $clientRepository + public readonly ClientRepository $clientRepository, + public readonly ClockInterface $clock, ) { } @@ -44,17 +46,31 @@ class ClientService return $this->clientRepository->save($client); } - public function deduceFromBalance(Client $client, int $amount) + public function deduceFromBalance(Client $client, int $amount): void { $client->deduceFromBalance($amount); $this->clientRepository->save($client); } - public function topUpBalance(Client $client, int $amount) + public function topUpBalance(Client $client, int $amount): void { $client->topUpBalance($amount); $this->clientRepository->save($client); } + + public function lock(Client $client): void + { + $client->setLockedAt($this->clock->now()); + + $this->clientRepository->save($client); + } + + public function unlock(Client $client): void + { + $client->setLockedAt(null); + + $this->clientRepository->save($client); + } } diff --git a/src/Service/OrderService.php b/src/Service/OrderService.php index 3bd33c6..ecc151f 100644 --- a/src/Service/OrderService.php +++ b/src/Service/OrderService.php @@ -3,6 +3,7 @@ namespace App\Service; use App\Aggregate\AcceptOrderCommand; +use App\Exception\ClientLockedException; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -16,8 +17,15 @@ readonly class OrderService ) { } + /** + * @throws \App\Exception\ClientLockedException + */ public function acceptOrder(AcceptOrderCommand $command) { + if ($command->client->isLocked()) { + throw new ClientLockedException(sprintf("Client '%s' is locked.", $command->client->getName())); + } + $this->crmClient->request('POST', '/order', [ 'body' => $this->serializer->serialize($command->order, format: 'json') ]); diff --git a/tests/Behat/ClientsContext.php b/tests/Behat/ClientsContext.php index 4916768..d85705a 100644 --- a/tests/Behat/ClientsContext.php +++ b/tests/Behat/ClientsContext.php @@ -47,6 +47,7 @@ class ClientsContext implements Context name: $row['name'], initialBalance: intval($row['initialBalance']), currentBalance: intval($row['currentBalance']), + lockedAt: $row['locked'] === 'yes' ? new \DateTimeImmutable() : null, ); $this->clientRepository->save($client); @@ -69,6 +70,22 @@ class ClientsContext implements Context Assert::assertNotNull($this->getClient($clientId)); } + /** + * @Given /^client with id "([^"]+)" should be locked$/ + */ + public function clientWithIdIsLocked(string $clientId) + { + Assert::assertTrue($this->getClient($clientId)->isLocked()); + } + + /** + * @Given /^client with id "([^"]+)" should not be locked$/ + */ + public function clientWithIdIsNotLocked(string $clientId) + { + Assert::assertFalse($this->getClient($clientId)->isLocked()); + } + /** * @Given /^client with id "([^"]+)" should have balance of (\d+)$/ */ @@ -76,7 +93,7 @@ class ClientsContext implements Context { $client = $this->getClient($clientId); - Assert::assertEquals($client->getBalance(), $balance); + Assert::assertEquals($balance, $client->getBalance()); } }