From cbfb10ec10c98b5de8f1424a2c56bf1effec7844 Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 14 Apr 2024 21:15:42 +0200 Subject: [PATCH] TASK-38: Deduce from balance after order --- features/accept-order.feature | 3 +- src/Aggregate/AcceptOrderCommand.php | 4 +- src/Contract/{OrderDto.php => Order.php} | 24 +++- .../{ProductDto.php => ProductEntry.php} | 20 ++- src/Controller/OrderController.php | 4 +- src/Entity/Client.php | 5 + src/Service/ClientService.php | 6 + src/Service/OrderService.php | 16 ++- src/Service/PricingStrategy.php | 15 +++ src/Service/TrivialPricingStrategy.php | 21 ++++ tests/App/Service/OrderServiceTest.php | 117 +++++++++++++----- .../Service/TrivialPricingStrategyTest.php | 46 +++++++ tests/Behat/CrmContext.php | 20 --- 13 files changed, 232 insertions(+), 69 deletions(-) rename src/Contract/{OrderDto.php => Order.php} (58%) rename src/Contract/{ProductDto.php => ProductEntry.php} (67%) create mode 100644 src/Service/PricingStrategy.php create mode 100644 src/Service/TrivialPricingStrategy.php create mode 100644 tests/App/Service/TrivialPricingStrategyTest.php delete mode 100644 tests/Behat/CrmContext.php diff --git a/features/accept-order.feature b/features/accept-order.feature index f87d895..c43c0ed 100644 --- a/features/accept-order.feature +++ b/features/accept-order.feature @@ -4,7 +4,7 @@ Feature: Background: Given there exist following clients: | clientId | name | initialBalance | currentBalance | - | 018edd2e-894a-78d7-b10c-16e05ca933a3 | Kacper | 10000 | 5000 | + | 018edd2e-894a-78d7-b10c-16e05ca933a3 | Kacper | 10000 | 200000 | Scenario: Frontend is able to send new orders Given the request has the following body: @@ -23,6 +23,7 @@ Feature: """ When I send a POST request to "/orders" Then the response status should be 200 + And client with id "018edd2e-894a-78d7-b10c-16e05ca933a3" should have balance of 71000 Scenario: Frontend must send properly formatted order request Given the request has the following body: diff --git a/src/Aggregate/AcceptOrderCommand.php b/src/Aggregate/AcceptOrderCommand.php index 6fea95c..af1b7bb 100644 --- a/src/Aggregate/AcceptOrderCommand.php +++ b/src/Aggregate/AcceptOrderCommand.php @@ -2,14 +2,14 @@ namespace App\Aggregate; -use App\Contract\OrderDto; +use App\Contract\Order; use App\Entity\Client; use Symfony\Component\Uid\Uuid; readonly class AcceptOrderCommand { public function __construct( - public OrderDto $order, + public Order $order, public Client $client, ) { } diff --git a/src/Contract/OrderDto.php b/src/Contract/Order.php similarity index 58% rename from src/Contract/OrderDto.php rename to src/Contract/Order.php index d8d29c9..5213701 100644 --- a/src/Contract/OrderDto.php +++ b/src/Contract/Order.php @@ -2,14 +2,16 @@ namespace App\Contract; +use App\Service\PricingStrategy; +use App\Service\TrivialPricingStrategy; use Symfony\Component\Uid\Uuid; -class OrderDto +class Order { public function __construct( private Uuid $orderId, private Uuid $clientId, - /** @var ProductDto[] */ + /** @var ProductEntry[] */ private array $products, ) { } @@ -42,18 +44,28 @@ class OrderDto public function setProducts(array $products): void { $this->products = array_combine( - array_map(fn (ProductDto $product) => $product->getProductId(), $products), + array_map(fn (ProductEntry $product) => $product->getProductId(), $products), array_values($products) ); } - public function addProduct(ProductDto $product): void + public function addProduct(ProductEntry $product): void { $this->products[$product->getProductId()] = $product; } - public function removeProduct(ProductDto $product): void + public function removeProduct(ProductEntry $product): void { - $this->products = array_filter($this->products, fn (ProductDto $other) => $other->getProductId() != $product->getProductId()); + $this->products = array_filter($this->products, fn (ProductEntry $other) => $other->getProductId() != $product->getProductId()); + } + + public function getTotalWeight(): float + { + return array_sum(array_map(fn (ProductEntry $product) => $product->getTotalWeight(), $this->products)); + } + + public function getTotalPrice(PricingStrategy $strategy): int + { + return $strategy->calculateTotalPriceOfOrder($this); } } diff --git a/src/Contract/ProductDto.php b/src/Contract/ProductEntry.php similarity index 67% rename from src/Contract/ProductDto.php rename to src/Contract/ProductEntry.php index 4676965..d006f31 100644 --- a/src/Contract/ProductDto.php +++ b/src/Contract/ProductEntry.php @@ -2,7 +2,10 @@ namespace App\Contract; -class ProductDto +use App\Service\PricingStrategy; +use App\Service\TrivialPricingStrategy; + +class ProductEntry { public function __construct( private string $productId, @@ -37,6 +40,11 @@ class ProductDto return $this->price; } + public function getPriceAsInt(): int + { + return round($this->getPrice() * 100); + } + public function setPrice(float $price): void { $this->price = $price; @@ -51,4 +59,14 @@ class ProductDto { $this->weight = $weight; } + + public function getTotalWeight(): float + { + return $this->getWeight() * $this->getQuantity(); + } + + public function getTotalPrice(PricingStrategy $strategy): float + { + return $strategy->calculateTotalPriceOfProductEntry($this); + } } diff --git a/src/Controller/OrderController.php b/src/Controller/OrderController.php index d134c99..34699c2 100644 --- a/src/Controller/OrderController.php +++ b/src/Controller/OrderController.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Controller; use App\Aggregate\AcceptOrderCommand; -use App\Contract\OrderDto; +use App\Contract\Order; use App\Exception\ClientNotExistsException; use App\Service\ClientService; use App\Service\OrderService; @@ -26,7 +26,7 @@ readonly class OrderController } #[Route(name: 'accept', methods: ['POST'])] - public function accept(#[MapRequestPayload] OrderDto $orderDto): Response + public function accept(#[MapRequestPayload] Order $orderDto): Response { try { $command = new AcceptOrderCommand( diff --git a/src/Entity/Client.php b/src/Entity/Client.php index 1e6d81f..23e17f1 100644 --- a/src/Entity/Client.php +++ b/src/Entity/Client.php @@ -56,4 +56,9 @@ class Client { return $this->currentBalance; } + + public function deduceFromBalance(int $amount): void + { + $this->currentBalance -= $amount; + } } diff --git a/src/Service/ClientService.php b/src/Service/ClientService.php index 27fffde..f83ba26 100644 --- a/src/Service/ClientService.php +++ b/src/Service/ClientService.php @@ -43,4 +43,10 @@ class ClientService return $this->clientRepository->save($client); } + + public function save(Client $client): void + { + $this->clientRepository->save($client); + } + } diff --git a/src/Service/OrderService.php b/src/Service/OrderService.php index 820de8a..fcf2f70 100644 --- a/src/Service/OrderService.php +++ b/src/Service/OrderService.php @@ -6,11 +6,13 @@ use App\Aggregate\AcceptOrderCommand; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; -class OrderService +readonly class OrderService { public function __construct( - private readonly HttpClientInterface $crmClient, - private readonly SerializerInterface $serializer, + private HttpClientInterface $crmClient, + private SerializerInterface $serializer, + private PricingStrategy $pricingStrategy, + private ClientService $clientService, ) { } @@ -19,5 +21,13 @@ class OrderService $this->crmClient->request('POST', '/order', [ 'body' => $this->serializer->serialize($command->order, format: 'json') ]); + + $totalPrice = $command->order->getTotalPrice($this->pricingStrategy); + + $command->client->deduceFromBalance($totalPrice); + + $this->clientService->save($command->client); } + + } diff --git a/src/Service/PricingStrategy.php b/src/Service/PricingStrategy.php new file mode 100644 index 0000000..133f93b --- /dev/null +++ b/src/Service/PricingStrategy.php @@ -0,0 +1,15 @@ +getPriceAsInt(); + + return $priceInBaseUnit * $product->getQuantity(); + } + + public function calculateTotalPriceOfOrder(Order $order): int + { + return array_sum(array_map($this->calculateTotalPriceOfProductEntry(...), $order->getProducts())); + } +} diff --git a/tests/App/Service/OrderServiceTest.php b/tests/App/Service/OrderServiceTest.php index 6a1e771..7c806ea 100644 --- a/tests/App/Service/OrderServiceTest.php +++ b/tests/App/Service/OrderServiceTest.php @@ -3,8 +3,8 @@ namespace App\Service; use App\Aggregate\AcceptOrderCommand; -use App\Contract\OrderDto; -use App\Contract\ProductDto; +use App\Contract\Order; +use App\Contract\ProductEntry; use App\Entity\Client; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\SerializerInterface; @@ -16,38 +16,6 @@ class OrderServiceTest extends TestCase private const ORDER_ID = '018eddae-ff52-7813-9f88-ada9e61a76f3'; private const CLIENT_ID = '018edd2e-894a-78d7-b10c-16e05ca933a3'; - private function createValidOrderDto(): OrderDto - { - return new OrderDto( - orderId: Uuid::fromRfc4122(self::ORDER_ID), - clientId: Uuid::fromRfc4122(self::CLIENT_ID), - products: [ - new ProductDto('p1', 1, 100, 100), - new ProductDto('p2', 2, 300, 100), - new ProductDto('p3', 3, 200, 100), - new ProductDto('p4', 4, 400, 700), - new ProductDto('p5', 5, 800, 700), - ] - ); - } - - public function testCrmGetsCalledOnValidOrderRequest() - { - $httpClientMock = $this->getMockBuilder(HttpClientInterface::class)->getMock(); - $serializerMock = $this->getMockBuilder(SerializerInterface::class)->getMock(); - - $sut = new OrderService($httpClientMock, $serializerMock); - - $httpClientMock->expects($this->once())->method('request')->with('POST', '/order'); - - $order = new AcceptOrderCommand( - $this->createValidOrderDto(), - $this->createClient(balance: 10000) - ); - - $sut->acceptOrder($order); - } - private function createClient(int $balance): Client { return new Client( @@ -58,4 +26,85 @@ class OrderServiceTest extends TestCase ); } + private function createValidOrderDto(): Order + { + return new Order( + orderId: Uuid::fromRfc4122(self::ORDER_ID), + clientId: Uuid::fromRfc4122(self::CLIENT_ID), + products: [ + new ProductEntry('p1', 1, 100.00, 100), + new ProductEntry('p2', 2, 300.00, 100), + new ProductEntry('p3', 3, 200.00, 100), + new ProductEntry('p4', 4, 400.00, 700), + new ProductEntry('p5', 5, 800.00, 700), + ] + ); + } + + private function createOrderService( + ?HttpClientInterface $httpClientMock = null, + ?SerializerInterface $serializerMock = null, + ?PricingStrategy $pricingStrategyMock = null, + ?ClientService $clientServiceMock = null, + ) { + $httpClientMock ??= $this->createMock(HttpClientInterface::class); + $serializerMock ??= $this->createMock(SerializerInterface::class); + $pricingStrategyMock ??= $this->createMock(PricingStrategy::class); + $clientServiceMock ??= $this->createMock(ClientService::class); + + return new OrderService($httpClientMock, $serializerMock, $pricingStrategyMock, $clientServiceMock); + } + + public function testCrmGetsCalledOnValidOrderRequest() + { + $httpClientMock = $this->createMock(HttpClientInterface::class); + $httpClientMock->expects($this->once())->method('request')->with('POST', '/order'); + + $order = new AcceptOrderCommand( + $this->createValidOrderDto(), + $this->createClient(balance: 100_00) + ); + + $sut = $this->createOrderService(httpClientMock: $httpClientMock); + $sut->acceptOrder($order); + } + + public function testTotalPriceIsDeducedFromClientBalance() + { + $initialBalance = 10000_00; + $price = 6900_00; + + $command = new AcceptOrderCommand( + $order = $this->createValidOrderDto(), + $client = $this->createClient($initialBalance), + ); + + $pricingStrategyMock = $this->createMock(PricingStrategy::class); + + $pricingStrategyMock + ->expects($this->atLeastOnce()) + ->method('calculateTotalPriceOfOrder') + ->with($order) + ->willReturn($price); + + $sut = $this->createOrderService(pricingStrategyMock: $pricingStrategyMock); + $sut->acceptOrder($command); + + $this->assertEquals($initialBalance, $client->getInitialBalance(), 'Initial balance got changed after placing order'); + $this->assertEquals($initialBalance - $price, $client->getBalance(), 'Current balance not changed after placing order'); + } + + public function testClientIsUpdatedAfterPlacingOrder() + { + $command = new AcceptOrderCommand( + $this->createValidOrderDto(), + $client = $this->createClient(10000_00), + ); + + $clientServiceMock = $this->createMock(ClientService::class); + $clientServiceMock->expects($this->once())->method('save')->with($client); + + $sut = $this->createOrderService(clientServiceMock: $clientServiceMock); + $sut->acceptOrder($command); + } } diff --git a/tests/App/Service/TrivialPricingStrategyTest.php b/tests/App/Service/TrivialPricingStrategyTest.php new file mode 100644 index 0000000..4530937 --- /dev/null +++ b/tests/App/Service/TrivialPricingStrategyTest.php @@ -0,0 +1,46 @@ +assertEquals(200, $sut->calculateTotalPriceOfProductEntry($product)); + } + + public function testCalculatingMultipleProductPrice() + { + $product = new ProductEntry(productId: 'p1', quantity: 5, price: 2.00, weight: 100); + + $sut = new TrivialPricingStrategy(); + + $this->assertEquals(1000, $sut->calculateTotalPriceOfProductEntry($product)); + } + + public function testCalculatingOrderPrice() + { + $order = new Order( + orderId: Uuid::v4(), + clientId: Uuid::v4(), + products: [ + new ProductEntry(productId: 'p1', quantity: 5, price: 2.00, weight: 100), + new ProductEntry(productId: 'p2', quantity: 2, price: 1.25, weight: 100), + ] + ); + + $sut = new TrivialPricingStrategy(); + + $this->assertEquals(1250, $sut->calculateTotalPriceOfOrder($order)); + } +} diff --git a/tests/Behat/CrmContext.php b/tests/Behat/CrmContext.php deleted file mode 100644 index ee147ff..0000000 --- a/tests/Behat/CrmContext.php +++ /dev/null @@ -1,20 +0,0 @@ -