diff --git a/src/BusinessLogic/AdminAPI/Aspects/ErrorHandlingAspect.php b/src/BusinessLogic/AdminAPI/Aspects/ErrorHandlingAspect.php index f2dcf824..266bcdca 100644 --- a/src/BusinessLogic/AdminAPI/Aspects/ErrorHandlingAspect.php +++ b/src/BusinessLogic/AdminAPI/Aspects/ErrorHandlingAspect.php @@ -7,6 +7,8 @@ use SeQura\Core\BusinessLogic\Bootstrap\Aspect\Aspect; use SeQura\Core\BusinessLogic\Domain\Connection\Exceptions\BadMerchantIdException; use SeQura\Core\BusinessLogic\Domain\Connection\Exceptions\WrongCredentialsException; +use SeQura\Core\BusinessLogic\Domain\Order\Exceptions\OrderNotFoundException; +use SeQura\Core\BusinessLogic\Domain\Order\Exceptions\TranslatableOrderNotFoundException; use SeQura\Core\BusinessLogic\Domain\Translations\Model\BaseTranslatableException; use SeQura\Core\BusinessLogic\Domain\Translations\Model\BaseTranslatableUnhandledException; use SeQura\Core\BusinessLogic\SeQuraAPI\Exceptions\HttpApiInvalidUrlParameterException; @@ -53,6 +55,17 @@ public function applyOn(callable $callee, array $params = []) ); $response = TranslatableErrorResponse::fromError(new WrongCredentialsException()); + } catch (OrderNotFoundException $e) { + Logger::logWarning( + $e->getMessage(), + 'Core', + [ + new LogContextData('message', $e->getMessage()), + new LogContextData('type', \get_class($e)), + ] + ); + + $response = TranslatableErrorResponse::fromError(new TranslatableOrderNotFoundException($e)); } catch (HttpApiInvalidUrlParameterException $e) { Logger::logError( $e->getMessage(), diff --git a/src/BusinessLogic/BootstrapComponent.php b/src/BusinessLogic/BootstrapComponent.php index 01fc9b12..8863f472 100644 --- a/src/BusinessLogic/BootstrapComponent.php +++ b/src/BusinessLogic/BootstrapComponent.php @@ -19,6 +19,7 @@ use SeQura\Core\BusinessLogic\CheckoutAPI\Checkout\Controller\CheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\ExpressCheckout\Controller\ExpressCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\CachedPaymentMethodsController; +use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\PaymentMethodsCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\PromotionalWidgets\PromotionalWidgetsCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\Solicitation\Controller\SolicitationController; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Controller\ConfigurationWebhookController; @@ -902,6 +903,15 @@ static function () { } ); + ServiceRegister::registerService( + PaymentMethodsCheckoutController::class, + static function () { + return new PaymentMethodsCheckoutController( + ServiceRegister::getService(OrderService::class) + ); + } + ); + ServiceRegister::registerService( PromotionalWidgetsCheckoutController::class, static function () { diff --git a/src/BusinessLogic/CheckoutAPI/CheckoutAPI.php b/src/BusinessLogic/CheckoutAPI/CheckoutAPI.php index 252395d3..02c877dc 100644 --- a/src/BusinessLogic/CheckoutAPI/CheckoutAPI.php +++ b/src/BusinessLogic/CheckoutAPI/CheckoutAPI.php @@ -10,6 +10,7 @@ use SeQura\Core\BusinessLogic\CheckoutAPI\ExpressCheckout\Controller\ExpressCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\Banners\BannerCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\CachedPaymentMethodsController; +use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\PaymentMethodsCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\PromotionalWidgets\PromotionalWidgetsCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\Solicitation\Controller\SolicitationController; @@ -60,6 +61,22 @@ public function cachedPaymentMethods(string $storeId): object ->beforeEachMethodOfService(CachedPaymentMethodsController::class); } + /** + * Payment methods of an order already solicited at SeQura, addressed by its reference. The methods a + * merchant offers before an order exists come from cachedPaymentMethods() instead. + * + * @param string $storeId + * + * @return object + */ + public function solicitedOrderPaymentMethods(string $storeId): object + { + return Aspects + ::run(new ErrorHandlingAspect()) + ->andRun(new StoreContextAspect($storeId)) + ->beforeEachMethodOfService(PaymentMethodsCheckoutController::class); + } + /** * @param string $storeId * diff --git a/src/BusinessLogic/CheckoutAPI/PaymentMethods/PaymentMethodsCheckoutController.php b/src/BusinessLogic/CheckoutAPI/PaymentMethods/PaymentMethodsCheckoutController.php new file mode 100644 index 00000000..36892330 --- /dev/null +++ b/src/BusinessLogic/CheckoutAPI/PaymentMethods/PaymentMethodsCheckoutController.php @@ -0,0 +1,59 @@ +orderService = $orderService; + } + + /** + * Returns the payment methods available for a solicited order, grouped in the categories SeQura returns them in. + * + * @param PaymentMethodsInCategoriesRequest $request + * + * @return PaymentMethodsInCategoriesResponse + * + * @throws HttpRequestException + * @throws OrderNotFoundException + * @throws OrderMerchantNotFoundException + * @throws ConnectionDataNotFoundException + * @throws CredentialsNotFoundException + * @throws DeploymentNotFoundException + */ + public function getPaymentMethodsInCategories( + PaymentMethodsInCategoriesRequest $request + ): PaymentMethodsInCategoriesResponse { + return new PaymentMethodsInCategoriesResponse( + $this->orderService->getAvailablePaymentMethodsInCategories($request->getOrderRef()) + ); + } +} diff --git a/src/BusinessLogic/CheckoutAPI/PaymentMethods/Requests/PaymentMethodsInCategoriesRequest.php b/src/BusinessLogic/CheckoutAPI/PaymentMethods/Requests/PaymentMethodsInCategoriesRequest.php new file mode 100644 index 00000000..6d88ec9b --- /dev/null +++ b/src/BusinessLogic/CheckoutAPI/PaymentMethods/Requests/PaymentMethodsInCategoriesRequest.php @@ -0,0 +1,32 @@ +orderRef = $orderRef; + } + + /** + * @return string + */ + public function getOrderRef(): string + { + return $this->orderRef; + } +} diff --git a/src/BusinessLogic/CheckoutAPI/PaymentMethods/Responses/PaymentMethodsInCategoriesResponse.php b/src/BusinessLogic/CheckoutAPI/PaymentMethods/Responses/PaymentMethodsInCategoriesResponse.php new file mode 100644 index 00000000..1a3653d9 --- /dev/null +++ b/src/BusinessLogic/CheckoutAPI/PaymentMethods/Responses/PaymentMethodsInCategoriesResponse.php @@ -0,0 +1,107 @@ +paymentMethodCategories = $paymentMethodCategories; + } + + /** + * @return SeQuraPaymentMethodCategory[] + */ + public function getPaymentMethodCategories(): array + { + return $this->paymentMethodCategories; + } + + /** + * Determines whether at least one category offers a payment method. Categories with no method are returned + * as well, so the presence of categories does not mean the buyer has anything to choose from. + * + * Only meaningful once isSuccessful() has passed: a failed call answers with an ErrorResponse, whose + * __call returns the response itself for every unknown method, so this reads as truthy rather than false. + * + * @return bool + */ + public function hasAvailablePaymentMethods(): bool + { + foreach ($this->paymentMethodCategories as $category) { + if (!empty($category->getMethods())) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + $categories = []; + foreach ($this->paymentMethodCategories as $category) { + $categories[] = [ + 'title' => $category->getTitle(), + 'description' => $category->getDescription(), + 'icon' => $category->getIcon(), + 'methods' => array_map([$this, 'paymentMethodToArray'], $category->getMethods()), + ]; + } + + return $categories; + } + + /** + * Serializes a payment method for the storefront. The fields are named as CachedPaymentMethodsResponse + * names them, so a storefront reads one shape whichever checkout endpoint it calls, and written out here + * rather than delegated to the model, whose own toArray() is the format the payment method is stored in. + * + * @param SeQuraPaymentMethod $paymentMethod + * + * @return mixed[] + */ + protected function paymentMethodToArray(SeQuraPaymentMethod $paymentMethod): array + { + return [ + 'product' => $paymentMethod->getProduct(), + 'title' => $paymentMethod->getTitle(), + 'longTitle' => $paymentMethod->getLongTitle(), + 'cost' => [ + 'setupFee' => $paymentMethod->getCost()->getSetupFee(), + 'instalmentFee' => $paymentMethod->getCost()->getInstalmentFee(), + 'downPaymentFees' => $paymentMethod->getCost()->getDownPaymentFees(), + 'instalmentTotal' => $paymentMethod->getCost()->getInstalmentTotal(), + ], + 'startsAt' => $paymentMethod->getStartsAt()->format('Y-m-d H:i:s'), + 'endsAt' => $paymentMethod->getEndsAt()->format('Y-m-d H:i:s'), + 'campaign' => $paymentMethod->getCampaign(), + 'claim' => $paymentMethod->getClaim(), + 'description' => $paymentMethod->getDescription(), + 'icon' => $paymentMethod->getIcon(), + 'costDescription' => $paymentMethod->getCostDescription(), + 'minAmount' => $paymentMethod->getMinAmount(), + 'maxAmount' => $paymentMethod->getMaxAmount(), + ]; + } +} diff --git a/src/BusinessLogic/Domain/Disconnect/Services/DisconnectService.php b/src/BusinessLogic/Domain/Disconnect/Services/DisconnectService.php index 997ec874..86110e71 100644 --- a/src/BusinessLogic/Domain/Disconnect/Services/DisconnectService.php +++ b/src/BusinessLogic/Domain/Disconnect/Services/DisconnectService.php @@ -252,14 +252,16 @@ private function removeAllDeploymentData(string $deploymentId): void // Removes country configurations connected to the deployment $countryConfigurations = $this->countryConfigurationRepository->getCountryConfiguration(); - $newCountyConfigurations = []; - foreach ($countryConfigurations as $countryConfiguration) { - if (!\in_array($countryConfiguration->getMerchantId(), $merchantIds, true)) { - $newCountyConfigurations[] = $countryConfiguration; + if ($countryConfigurations) { + $newCountyConfigurations = []; + foreach ($countryConfigurations as $countryConfiguration) { + if (!\in_array($countryConfiguration->getMerchantId(), $merchantIds, true)) { + $newCountyConfigurations[] = $countryConfiguration; + } } - } - $this->countryConfigurationRepository->setCountryConfiguration($newCountyConfigurations); + $this->countryConfigurationRepository->setCountryConfiguration($newCountyConfigurations); + } // Removes all payment methods connected to the deployment foreach ($merchantIds as $merchantId) { diff --git a/src/BusinessLogic/Domain/Order/Exceptions/OrderMerchantNotFoundException.php b/src/BusinessLogic/Domain/Order/Exceptions/OrderMerchantNotFoundException.php new file mode 100644 index 00000000..c5a3eddf --- /dev/null +++ b/src/BusinessLogic/Domain/Order/Exceptions/OrderMerchantNotFoundException.php @@ -0,0 +1,18 @@ +proxy->getAvailablePaymentMethods( new GetAvailablePaymentMethodsRequest( $order->getReference(), - $order->getMerchant()->getId() + $this->getOrderMerchantId($order) ) ); } @@ -165,15 +168,28 @@ public function getAvailablePaymentMethods(SeQuraOrder $order): array /** * Gets available payment methods for solicited order in categories. * + * The merchant stays an argument for callers that hold it, which keeps the order out of the lookup: an order + * solicited but not persisted by the integration is answered for as it always was. Omitting it reads the + * merchant off the stored order instead, and then requires the order to be there. + * * @param string $orderRef - * @param string $merchantId + * @param string $merchantId Merchant the order was solicited for. Read from the stored order when omitted. * * @return SeQuraPaymentMethodCategory[] * * @throws HttpRequestException + * @throws OrderNotFoundException + * @throws OrderMerchantNotFoundException + * @throws ConnectionDataNotFoundException + * @throws CredentialsNotFoundException + * @throws DeploymentNotFoundException */ - public function getAvailablePaymentMethodsInCategories(string $orderRef, string $merchantId): array + public function getAvailablePaymentMethodsInCategories(string $orderRef, string $merchantId = ''): array { + if ($merchantId === '') { + $merchantId = $this->getOrderMerchantId($this->getSeQuraOrder($orderRef)); + } + return $this->proxy->getAvailablePaymentMethodsInCategories( new GetAvailablePaymentMethodsRequest($orderRef, $merchantId) ); @@ -472,6 +488,31 @@ public function getSeQuraOrder(string $orderReference): SeQuraOrder return $seQuraOrder; } + /** + * Returns the merchant the order was solicited for, in the form the proxy request expects. + * + * The id is untyped on the merchant, so an order whose merchant record lost it would otherwise reach the + * proxy as an empty string and fail in the credentials lookup, with nothing pointing back at the order. + * + * @param SeQuraOrder $order + * + * @return string + * + * @throws OrderMerchantNotFoundException + */ + private function getOrderMerchantId(SeQuraOrder $order): string + { + $merchantId = (string)$order->getMerchant()->getId(); + + if ($merchantId === '') { + throw new OrderMerchantNotFoundException( + "SeQura order with reference {$order->getReference()} carries no merchant id." + ); + } + + return $merchantId; + } + /** * Returns PaymentMethod information for SeQura order. * @@ -482,16 +523,14 @@ public function getSeQuraOrder(string $orderReference): SeQuraOrder * @return PaymentMethod|null * * @throws HttpRequestException + * @throws OrderNotFoundException|OrderMerchantNotFoundException */ private function getOrderPaymentMethodInfo( string $orderReference, string $paymentMethodId, string $merchantId ): ?PaymentMethod { - $methodCategories = $this->getAvailablePaymentMethodsInCategories( - $orderReference, - $merchantId - ); + $methodCategories = $this->getAvailablePaymentMethodsInCategories($orderReference, $merchantId); foreach ($methodCategories as $category) { foreach ($category->getMethods() as $method) { diff --git a/tests/BusinessLogic/CheckoutAPI/PaymentMethods/PaymentMethodsCheckoutApiTest.php b/tests/BusinessLogic/CheckoutAPI/PaymentMethods/PaymentMethodsCheckoutApiTest.php new file mode 100644 index 00000000..51d65bce --- /dev/null +++ b/tests/BusinessLogic/CheckoutAPI/PaymentMethods/PaymentMethodsCheckoutApiTest.php @@ -0,0 +1,220 @@ +orderService = $this->createMock(OrderService::class); + TestServiceRegister::registerService(OrderService::class, function () { + return $this->orderService; + }); + } + + public function testGetPaymentMethodsInCategoriesReturnsCategories(): void + { + // Arrange + $this->orderService->method('getAvailablePaymentMethodsInCategories')->willReturn([ + new SeQuraPaymentMethodCategory('Paga Después', 'Paga después', 'pay_later.svg', [ + $this->paymentMethod('i1', 'Paga Después') + ]) + ]); + + // Act + $response = CheckoutAPI::get()->solicitedOrderPaymentMethods('1') + ->getPaymentMethodsInCategories(new PaymentMethodsInCategoriesRequest('testOrderRef')); + + // Assert + self::assertTrue($response->isSuccessful()); + self::assertEquals([ + [ + 'title' => 'Paga Después', + 'description' => 'Paga después', + 'icon' => 'pay_later.svg', + 'methods' => [ + [ + 'product' => 'i1', + 'title' => 'Paga Después', + 'longTitle' => 'Paga Después', + 'cost' => [ + 'setupFee' => 0, + 'instalmentFee' => 0, + 'downPaymentFees' => 0, + 'instalmentTotal' => 0, + ], + 'startsAt' => '2000-02-22 21:22:00', + 'endsAt' => '2222-02-22 21:22:00', + 'campaign' => null, + 'claim' => null, + 'description' => null, + 'icon' => null, + 'costDescription' => null, + 'minAmount' => null, + 'maxAmount' => null, + ] + ], + ] + ], $response->toArray()); + } + + public function testGetPaymentMethodsInCategoriesReturnsCategoryModels(): void + { + // Arrange + $category = new SeQuraPaymentMethodCategory('Paga Después', 'Paga después', 'pay_later.svg', [ + $this->paymentMethod('i1', 'Paga Después') + ]); + $this->orderService->method('getAvailablePaymentMethodsInCategories')->willReturn([$category]); + + // Act + $response = CheckoutAPI::get()->solicitedOrderPaymentMethods('1') + ->getPaymentMethodsInCategories(new PaymentMethodsInCategoriesRequest('testOrderRef')); + + // Assert + // Integrations rendering the categories themselves read the models rather than the serialized payload. + self::assertSame([$category], $response->getPaymentMethodCategories()); + } + + public function testGetPaymentMethodsInCategoriesOnlyNeedsOrderReference(): void + { + // Arrange + $this->orderService->expects(self::once()) + ->method('getAvailablePaymentMethodsInCategories') + ->with('testOrderRef') + ->willReturn([]); + + // Act + $response = CheckoutAPI::get()->solicitedOrderPaymentMethods('1') + ->getPaymentMethodsInCategories(new PaymentMethodsInCategoriesRequest('testOrderRef')); + + // Assert + self::assertTrue($response->isSuccessful()); + } + + public function testGetPaymentMethodsInCategoriesNoCategories(): void + { + // Arrange + $this->orderService->method('getAvailablePaymentMethodsInCategories')->willReturn([]); + + // Act + $response = CheckoutAPI::get()->solicitedOrderPaymentMethods('1') + ->getPaymentMethodsInCategories(new PaymentMethodsInCategoriesRequest('testOrderRef')); + + // Assert + self::assertTrue($response->isSuccessful()); + self::assertEmpty($response->toArray()); + self::assertFalse($response->hasAvailablePaymentMethods()); + } + + public function testGetPaymentMethodsInCategoriesCategoryWithoutMethods(): void + { + // Arrange + $this->orderService->method('getAvailablePaymentMethodsInCategories')->willReturn([ + new SeQuraPaymentMethodCategory('Paga Después', 'Paga después', null, []) + ]); + + // Act + $response = CheckoutAPI::get()->solicitedOrderPaymentMethods('1') + ->getPaymentMethodsInCategories(new PaymentMethodsInCategoriesRequest('testOrderRef')); + + // Assert + self::assertTrue($response->isSuccessful()); + self::assertNotEmpty($response->toArray()); + self::assertFalse($response->hasAvailablePaymentMethods()); + } + + public function testGetPaymentMethodsInCategoriesOrderNotFound(): void + { + // Arrange + $this->orderService->method('getAvailablePaymentMethodsInCategories') + ->willThrowException(new OrderNotFoundException('SeQura order with reference testOrderRef is not found.')); + + // Act + $response = CheckoutAPI::get()->solicitedOrderPaymentMethods('1') + ->getPaymentMethodsInCategories(new PaymentMethodsInCategoriesRequest('testOrderRef')); + + // Assert + self::assertFalse($response->isSuccessful()); + self::assertSame(404, $response->toArray()['statusCode']); + self::assertSame('general.errors.order.notFound', $response->toArray()['errorCode']); + } + + public function testGetPaymentMethodsInCategoriesApiFailure(): void + { + // Arrange + $this->orderService->method('getAvailablePaymentMethodsInCategories') + ->willThrowException(new HttpRequestException('Request failed.')); + + // Act + $response = CheckoutAPI::get()->solicitedOrderPaymentMethods('1') + ->getPaymentMethodsInCategories(new PaymentMethodsInCategoriesRequest('testOrderRef')); + + // Assert + self::assertFalse($response->isSuccessful()); + self::assertSame(0, $response->toArray()['statusCode']); + self::assertSame('general.errors.unknown', $response->toArray()['errorCode']); + } + + public function testHasAvailablePaymentMethodsIsNoGuardOnAFailedCall(): void + { + // Arrange + $this->orderService->method('getAvailablePaymentMethodsInCategories') + ->willThrowException(new HttpRequestException('Request failed.')); + + // Act + $response = CheckoutAPI::get()->solicitedOrderPaymentMethods('1') + ->getPaymentMethodsInCategories(new PaymentMethodsInCategoriesRequest('testOrderRef')); + + // Assert + // A failed call answers with an ErrorResponse, whose __call returns the response itself for every + // unknown method. The helper therefore reads as truthy, and says nothing until isSuccessful() passed. + self::assertFalse($response->isSuccessful()); + self::assertSame($response, $response->hasAvailablePaymentMethods()); + } + + /** + * @param string $product + * @param string $title + * + * @return SeQuraPaymentMethod + * + * @throws Exception + */ + private function paymentMethod(string $product, string $title): SeQuraPaymentMethod + { + return new SeQuraPaymentMethod( + $product, + $title, + $title, + 'pay_later', + new SeQuraCost(0, 0, 0, 0), + new DateTime('2000-02-22T21:22:00Z'), + new DateTime('2222-02-22T21:22:00Z') + ); + } +} diff --git a/tests/BusinessLogic/CheckoutAPI/Solicitation/MockComponents/MockOrderProxy.php b/tests/BusinessLogic/CheckoutAPI/Solicitation/MockComponents/MockOrderProxy.php index af3e9be2..4aa53d90 100644 --- a/tests/BusinessLogic/CheckoutAPI/Solicitation/MockComponents/MockOrderProxy.php +++ b/tests/BusinessLogic/CheckoutAPI/Solicitation/MockComponents/MockOrderProxy.php @@ -47,6 +47,10 @@ class MockOrderProxy implements OrderProxyInterface * @var int */ private $getFormCallCount = 0; + /** + * @var GetAvailablePaymentMethodsRequest|null + */ + private $lastPaymentMethodsInCategoriesRequest; /** * @param ?SeQuraOrder $order @@ -103,9 +107,19 @@ public function getAvailablePaymentMethods(GetAvailablePaymentMethodsRequest $re public function getAvailablePaymentMethodsInCategories(GetAvailablePaymentMethodsRequest $request): array { + $this->lastPaymentMethodsInCategoriesRequest = $request; + return []; } + /** + * @return GetAvailablePaymentMethodsRequest|null + */ + public function getLastPaymentMethodsInCategoriesRequest(): ?GetAvailablePaymentMethodsRequest + { + return $this->lastPaymentMethodsInCategoriesRequest; + } + public function createOrder(CreateOrderRequest $request): SeQuraOrder { if ($this->createOrderException !== null) { diff --git a/tests/BusinessLogic/Common/BaseTestCase.php b/tests/BusinessLogic/Common/BaseTestCase.php index 92f0ea03..50c8e778 100644 --- a/tests/BusinessLogic/Common/BaseTestCase.php +++ b/tests/BusinessLogic/Common/BaseTestCase.php @@ -19,6 +19,7 @@ use SeQura\Core\BusinessLogic\CheckoutAPI\Banners\BannerCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\Checkout\Controller\CheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\CachedPaymentMethodsController; +use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\PaymentMethodsCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\ExpressCheckout\Controller\ExpressCheckoutController; use SeQura\Core\BusinessLogic\CheckoutAPI\PromotionalWidgets\PromotionalWidgetsCheckoutController; use SeQura\Core\BusinessLogic\ConfigurationWebhookAPI\Controller\ConfigurationWebhookController; @@ -499,6 +500,11 @@ protected function setUp(): void TestServiceRegister::getService(PaymentMethodsService::class) ); }, + PaymentMethodsCheckoutController::class => function () { + return new PaymentMethodsCheckoutController( + TestServiceRegister::getService(OrderService::class) + ); + }, PromotionalWidgetsCheckoutController::class => function () { return new PromotionalWidgetsCheckoutController( TestServiceRegister::getService(WidgetSettingsService::class), diff --git a/tests/BusinessLogic/Domain/Disconnect/Services/DisconnectServiceTest.php b/tests/BusinessLogic/Domain/Disconnect/Services/DisconnectServiceTest.php index 0526486a..e0c82df3 100644 --- a/tests/BusinessLogic/Domain/Disconnect/Services/DisconnectServiceTest.php +++ b/tests/BusinessLogic/Domain/Disconnect/Services/DisconnectServiceTest.php @@ -374,6 +374,56 @@ public function testDisconnectNotFull(): void ); } + /** + * A partial disconnect of a deployment nothing is configured for must leave the country configuration + * alone: the repository answers with null when no entity is stored, and writing back an empty list + * would create one. + * + * @return void + * + * @throws Exception + */ + public function testDisconnectNotFullWithoutCountryConfigurations(): void + { + //Arrange + $this->credentialsRepository->setCredentials([ + new Credentials('logeecom1', 'PT', 'EUR', 'assetsKey1', [], 'sequra'), + ]); + $this->countryConfigurationRepository->deleteCountryConfigurations(); + + //Act + $this->service->disconnect('sequra', false); + + //Assert + self::assertNull($this->countryConfigurationRepository->getCountryConfiguration()); + } + + /** + * When every stored country configuration belongs to the disconnected deployment, all of them are + * removed: the configurations are gone, but the entity holding them stays. + * + * @return void + * + * + * @throws Exception + */ + public function testDisconnectNotFullRemovesAllCountryConfigurations(): void + { + //Arrange + $this->credentialsRepository->setCredentials([ + new Credentials('logeecom1', 'PT', 'EUR', 'assetsKey1', [], 'sequra'), + ]); + $this->countryConfigurationRepository->setCountryConfiguration([ + new CountryConfiguration('PT', 'logeecom1'), + ]); + + //Act + $this->service->disconnect('sequra', false); + + //Assert + self::assertSame([], $this->countryConfigurationRepository->getCountryConfiguration()); + } + /** * @return void * diff --git a/tests/BusinessLogic/Domain/Order/Services/OrderServiceTest.php b/tests/BusinessLogic/Domain/Order/Services/OrderServiceTest.php index bc0e9c16..2ec093c1 100644 --- a/tests/BusinessLogic/Domain/Order/Services/OrderServiceTest.php +++ b/tests/BusinessLogic/Domain/Order/Services/OrderServiceTest.php @@ -10,6 +10,8 @@ use SeQura\Core\BusinessLogic\Domain\Integration\Order\OrderCreationInterface; use SeQura\Core\BusinessLogic\Domain\Multistore\StoreContext; use SeQura\Core\BusinessLogic\Domain\Order\Exceptions\InvalidCartItemsException; +use SeQura\Core\BusinessLogic\Domain\Order\Exceptions\OrderMerchantNotFoundException; +use SeQura\Core\BusinessLogic\Domain\Order\Exceptions\OrderNotFoundException; use SeQura\Core\BusinessLogic\Domain\Order\Models\OrderRequest\Address; use SeQura\Core\BusinessLogic\Domain\Order\Models\OrderRequest\Cart; use SeQura\Core\BusinessLogic\Domain\Order\Models\OrderRequest\CreateOrderRequest; @@ -389,8 +391,10 @@ public function testGetPaymentMethodsInCategoriesSuccessfulResponse(): void __DIR__ . '/../../../Common/ApiResponses/Order/GetPaymentMethodsResponses/SuccessfulResponse.json' ); + $this->storeOrderWithMerchant('testMerchantId'); + $this->httpClient->setMockResponses([new HttpResponse(200, [], $rawResponseBody)]); - $response = $this->orderService->getAvailablePaymentMethodsInCategories('testId', 'testMerchantId'); + $response = $this->orderService->getAvailablePaymentMethodsInCategories('testId'); $responseBody = json_decode($rawResponseBody, true); $paymentMethodCategories = []; @@ -472,6 +476,84 @@ public function testGetPaymentMethodsInCategoriesSuccessfulResponse(): void } } + /** + * @return void + * + * @throws Exception + */ + public function testGetPaymentMethodsInCategoriesUsesMerchantOfStoredOrder(): void + { + // Arrange + $this->useMockOrderProxy(); + + $this->storeOrderWithMerchant('merchantOfTheOrder'); + + // Act + $this->orderService->getAvailablePaymentMethodsInCategories('testId'); + + // Assert + $proxyRequest = $this->orderProxy->getLastPaymentMethodsInCategoriesRequest(); + self::assertNotNull($proxyRequest); + self::assertEquals('testId', $proxyRequest->getOrderId()); + self::assertEquals('merchantOfTheOrder', $proxyRequest->getMerchantId()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testGetPaymentMethodsInCategoriesUsesSuppliedMerchantWithoutTheStoredOrder(): void + { + // Arrange + $this->useMockOrderProxy(); + + // Act + $this->orderService->getAvailablePaymentMethodsInCategories('testId', 'merchantOfTheCaller'); + + // Assert + $proxyRequest = $this->orderProxy->getLastPaymentMethodsInCategoriesRequest(); + self::assertNotNull($proxyRequest); + self::assertEquals('testId', $proxyRequest->getOrderId()); + self::assertEquals('merchantOfTheCaller', $proxyRequest->getMerchantId()); + } + + /** + * @return void + * + * @throws Exception + */ + public function testGetPaymentMethodsInCategoriesForOrderWithoutMerchant(): void + { + // Arrange + $this->useMockOrderProxy(); + + $this->storeOrderWithMerchant(''); + + // Assert + $this->expectException(OrderMerchantNotFoundException::class); + + // Act + $this->orderService->getAvailablePaymentMethodsInCategories('testId'); + } + + /** + * @return void + * + * @throws Exception + */ + public function testGetPaymentMethodsInCategoriesForUnknownOrder(): void + { + // Arrange + $this->useMockOrderProxy(); + + // Assert + $this->expectException(OrderNotFoundException::class); + + // Act + $this->orderService->getAvailablePaymentMethodsInCategories('unknownOrderRef'); + } + /** * @return void * @@ -799,6 +881,45 @@ private function expectedDeliveryAddressToArrayResponse(): array ]; } + /** + * Rebuilds the service on a proxy and a repository that record what they were given, for tests asserting + * on the request that left rather than on the response that came back. + * + * @return void + */ + private function useMockOrderProxy(): void + { + $this->orderProxy = new MockOrderProxy(); + $this->orderRepository = new MockSeQuraOrderRepository(); + $this->orderService = new OrderService( + $this->orderProxy, + $this->orderRepository, + $this->merchantOrderBuilder, + TestServiceRegister::getService(OrderCreationInterface::class) + ); + } + + /** + * Stores the mock order under the reference the payment method tests look it up by, solicited for the + * given merchant. + * + * @param string $merchantId + * + * @return void + * + * @throws Exception + */ + private function storeOrderWithMerchant(string $merchantId): void + { + $order = json_decode(file_get_contents(__DIR__ . '/../../../Common/MockObjects/SeQuraOrder.json'), true); + $order['order']['merchant']['id'] = $merchantId; + + $seQuraOrder = SeQuraOrder::fromArray($order['order']); + $seQuraOrder->setReference('testId'); + + $this->orderRepository->setSeQuraOrder($seQuraOrder); + } + private function expectedInvoiceAddressToArrayResponse(): array { return [ diff --git a/tests/Infrastructure/ORM/AbstractGenericQueueItemRepositoryTest.php b/tests/Infrastructure/ORM/AbstractGenericQueueItemRepositoryTest.php index d3db0580..bbd879f8 100644 --- a/tests/Infrastructure/ORM/AbstractGenericQueueItemRepositoryTest.php +++ b/tests/Infrastructure/ORM/AbstractGenericQueueItemRepositoryTest.php @@ -324,7 +324,7 @@ protected function readQueueItemsFromFile() $queueItem->setProgressBasePoints($item['progress']); $queueItem->setLastExecutionProgressBasePoints($item['lastExecutionProgress']); $queueItem->setRetries($item['retries']); - $queueItem->setFailureDescription($item['failureDescription']); + $queueItem->setFailureDescription($item['failureDescription'] ?? ''); $queueItem->setSerializedTask(Serializer::serialize($task)); $queueItem->setCreateTimestamp($item['createTimestamp']); $queueItem->setQueueTimestamp($item['queueTimestamp']);