diff --git a/src/Endpoints/Merchants.php b/src/Endpoints/Merchants.php index 995ee04a..8ff4156f 100644 --- a/src/Endpoints/Merchants.php +++ b/src/Endpoints/Merchants.php @@ -25,8 +25,11 @@ namespace Alma\API\Endpoints; +use Alma\API\Entities\DTO\MerchantBusinessEvent\CartInitiatedBusinessEvent; +use Alma\API\Entities\DTO\MerchantBusinessEvent\OrderConfirmedBusinessEvent; use Alma\API\Entities\FeePlan; use Alma\API\Entities\Merchant; +use Alma\API\Exceptions\RequestException; use Alma\API\RequestError; class Merchants extends Base @@ -80,4 +83,61 @@ public function feePlans($kind = FeePlan::KIND_GENERAL, $installmentsCounts = "a return new FeePlan($val); }, $res->json); } + + /** + * Prepare and send a business event for a cart initiated + * + * @param CartInitiatedBusinessEvent $cartEventData + * @return void + * @throws RequestException + */ + public function sendCartInitiatedBusinessEvent(CartInitiatedBusinessEvent $cartEventData) + { + $cartEventDataPayload = [ + 'event_type' => $cartEventData->getEventType(), + 'cart_id' => $cartEventData->getCartId() + ]; + $this->sendBusinessEvent($cartEventDataPayload); + } + + /** + * Prepare and send a business event for Order confirmed + * + * @param OrderConfirmedBusinessEvent $orderConfirmedBusinessEvent + * @return void + * @throws RequestException + */ + public function sendOrderConfirmedBusinessEvent(OrderConfirmedBusinessEvent $orderConfirmedBusinessEvent) + { + $cartEventDataPayload = [ + 'event_type' => $orderConfirmedBusinessEvent->getEventType(), + 'is_alma_p1x' => $orderConfirmedBusinessEvent->isAlmaP1X(), + 'is_alma_bnpl' => $orderConfirmedBusinessEvent->isAlmaBNPL(), + 'was_bnpl_eligible' => $orderConfirmedBusinessEvent->wasBNPLEligible(), + 'order_id' => $orderConfirmedBusinessEvent->getOrderId(), + 'cart_id' => $orderConfirmedBusinessEvent->getCartId(), + 'alma_payment_id' => $orderConfirmedBusinessEvent->getAlmaPaymentId() + ]; + $this->sendBusinessEvent($cartEventDataPayload); + } + + /** + * Send merchant_business_event and return 204 no content + * + * @param array $eventData + * @return void + * @throws RequestException + */ + private function sendBusinessEvent($eventData) + { + try { + $res = $this->request(self::ME_PATH . "/business-events")->setRequestBody($eventData)->post(); + } catch (RequestError $e) { + throw new RequestException($e->getErrorMessage(), null); + } + if ($res->isError()) { + throw new RequestException($res->errorMessage, null, $res); + } + } + } diff --git a/src/Entities/DTO/MerchantBusinessEvent/AbstractBusinessEvent.php b/src/Entities/DTO/MerchantBusinessEvent/AbstractBusinessEvent.php new file mode 100644 index 00000000..3e561c56 --- /dev/null +++ b/src/Entities/DTO/MerchantBusinessEvent/AbstractBusinessEvent.php @@ -0,0 +1,22 @@ +eventType; + } + +} diff --git a/src/Entities/DTO/MerchantBusinessEvent/CartInitiatedBusinessEvent.php b/src/Entities/DTO/MerchantBusinessEvent/CartInitiatedBusinessEvent.php new file mode 100644 index 00000000..91e4683e --- /dev/null +++ b/src/Entities/DTO/MerchantBusinessEvent/CartInitiatedBusinessEvent.php @@ -0,0 +1,36 @@ +eventType = 'cart_initiated'; + if(empty($cartId) || !is_string($cartId)) { + throw new ParametersException('CartId must be a string'); + } + $this->cartId = $cartId; + } + + /** + * Get Cart Id + * + * @return string + */ + public function getCartId() + { + return $this->cartId; + } +} diff --git a/src/Entities/DTO/MerchantBusinessEvent/OrderConfirmedBusinessEvent.php b/src/Entities/DTO/MerchantBusinessEvent/OrderConfirmedBusinessEvent.php new file mode 100644 index 00000000..6c812298 --- /dev/null +++ b/src/Entities/DTO/MerchantBusinessEvent/OrderConfirmedBusinessEvent.php @@ -0,0 +1,151 @@ +eventType = 'order_confirmed'; + $this->almaP1XStatus = $isAlmaP1X; + $this->almaBNPLStatus = $isAlmaBNPL; + $this->wasBNPLEligible = $wasBNPLEligible; + $this->orderId = $orderId; + $this->cartId = $cartId; + $this->almaPaymentId = $almaPaymentId; + $this->validateData(); + } + + /** + * @return bool + */ + public function isAlmaP1X() + { + return $this->almaP1XStatus; + } + + /** + * @return bool + */ + public function isAlmaBNPL() + { + return $this->almaBNPLStatus; + } + + /** + * Was eligible at the time of payment + * + * @return bool + */ + public function wasBNPLEligible() + { + return $this->wasBNPLEligible; + } + + /** + * @return string + */ + public function getOrderId() + { + return $this->orderId; + } + + /** + * @return string + */ + public function getCartId() + { + return $this->cartId; + } + + /** + * @return string | null + */ + public function getAlmaPaymentId() + { + return $this->almaPaymentId; + } + + /** + * Check if it is an Alma payment + * + * @return bool + */ + public function isAlmaPayment() + { + return $this->almaP1XStatus || $this->almaBNPLStatus; + } + + /** + * @return void + * @throws ParametersException + */ + protected function validateData() + { + if( + !is_bool($this->almaP1XStatus) || + !is_bool($this->almaBNPLStatus) || + !is_bool($this->wasBNPLEligible) || + !is_string($this->orderId) || + !is_string($this->cartId) || + // Alma payment id should be absent for non Alma payments + (!$this->isAlmaPayment() && !is_null($this->almaPaymentId)) + ) + { + throw new ParametersException('Invalid data type in OrderConfirmedBusinessEvent constructor'); + } + + //Alma payment id for Alma payment, Must be a string + if( + $this->isAlmaPayment() && + !is_string($this->almaPaymentId) + ) + { + throw new ParametersException('Alma payment id is mandatory for Alma payment'); + } + } + + +} \ No newline at end of file diff --git a/tests/Unit/Endpoints/MerchantsTest.php b/tests/Unit/Endpoints/MerchantsTest.php new file mode 100644 index 00000000..99b87d10 --- /dev/null +++ b/tests/Unit/Endpoints/MerchantsTest.php @@ -0,0 +1,173 @@ +clientContext = Mockery::mock(ClientContext::class); + $this->merchantEndpoint = Mockery::mock(Merchants::class)->makePartial(); + $loggerMock = Mockery::mock(LoggerInterface::class); + $loggerMock->shouldReceive('error'); + $this->requestObject = Mockery::mock(Request::class); + $this->responseMock = Mockery::mock(Response::class); + $this->clientContext->logger = $loggerMock; + $this->merchantEndpoint->setClientContext($this->clientContext); + } + + public function tearDown(): void + { + $this->merchantEndpoint = null; + $this->responseMock = null; + $this->requestObject = null; + $this->clientContext = null; + Mockery::close(); + } + + public function testSendBusinessEventPostThrowRequestErrorThrowRequestException() + { + $this->merchantEndpoint->shouldReceive('request') + ->with('/v1/me/business-events') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('setRequestBody') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('post') + ->once() + ->andThrow(new RequestError('Error in post', null, null)); + $this->expectException(RequestException::class); + $this->merchantEndpoint->sendCartInitiatedBusinessEvent(new CartInitiatedBusinessEvent('42')); + } + + public function testSendBusinessEventBadResponseRequestException() + { + $this->merchantEndpoint->shouldReceive('request') + ->with('/v1/me/business-events') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('setRequestBody') + ->once() + ->andReturn($this->requestObject); + $this->responseMock->errorMessage = 'Error in response'; + $this->responseMock->shouldReceive('isError') + ->once() + ->andReturn(true); + $this->requestObject->shouldReceive('post') + ->once() + ->andReturn($this->responseMock); + $this->expectException(RequestException::class); + $this->merchantEndpoint->sendCartInitiatedBusinessEvent(new CartInitiatedBusinessEvent('42')); + } + + public function testSendCartInitiatedEvent() + { + $cartInitiatedEvent = new CartInitiatedBusinessEvent('42'); + $this->merchantEndpoint->shouldReceive('request') + ->with('/v1/me/business-events') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('setRequestBody') + ->with(['event_type' => $cartInitiatedEvent->getEventType(), 'cart_id' => $cartInitiatedEvent->getCartId()]) + ->once() + ->andReturn($this->requestObject); + $this->responseMock->shouldReceive('isError')->once()->andReturn(false); + $this->requestObject->shouldReceive('post')->once()->andReturn($this->responseMock); + $this->assertNull($this->merchantEndpoint->sendCartInitiatedBusinessEvent($cartInitiatedEvent)); + } + + public function testSendOrderConfirmedBusinessEventForNonAlmaPayment() + { + $orderConfirmedBusinessEvent = new OrderConfirmedBusinessEvent( + false, + false, + true, + '42', + '54' + ); + $this->merchantEndpoint->shouldReceive('request') + ->with('/v1/me/business-events') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('setRequestBody') + ->with( + [ + 'event_type' => 'order_confirmed', + 'is_alma_p1x' => false, + 'is_alma_bnpl' => false, + 'was_bnpl_eligible' => true, + 'order_id' => '42', + 'cart_id' => '54', + 'alma_payment_id' => NULL + ] + ) + ->once() + ->andReturn($this->requestObject); + $this->responseMock->shouldReceive('isError')->once()->andReturn(false); + $this->requestObject->shouldReceive('post')->once()->andReturn($this->responseMock); + $this->assertNull($this->merchantEndpoint->sendOrderConfirmedBusinessEvent($orderConfirmedBusinessEvent)); + } + + public function testSendOrderConfirmedBusinessEventForAlmaPayment() + { + $orderConfirmedBusinessEvent = new OrderConfirmedBusinessEvent( + true, + false, + true, + '42', + '54', + 'alma_payment_id' + ); + $this->merchantEndpoint->shouldReceive('request') + ->with('/v1/me/business-events') + ->once() + ->andReturn($this->requestObject); + $this->requestObject->shouldReceive('setRequestBody') + ->with( + [ + 'event_type' => 'order_confirmed', + 'is_alma_p1x' => true, + 'is_alma_bnpl' => false, + 'was_bnpl_eligible' => true, + 'order_id' => '42', + 'cart_id' => '54', + 'alma_payment_id' => 'alma_payment_id' + ] + ) + ->once() + ->andReturn($this->requestObject); + $this->responseMock->shouldReceive('isError')->once()->andReturn(false); + $this->requestObject->shouldReceive('post')->once()->andReturn($this->responseMock); + $this->assertNull($this->merchantEndpoint->sendOrderConfirmedBusinessEvent($orderConfirmedBusinessEvent)); + } +} diff --git a/tests/Unit/Entities/DTO/MerchantBusinessEvent/CartInitiatedBusinessEventTest.php b/tests/Unit/Entities/DTO/MerchantBusinessEvent/CartInitiatedBusinessEventTest.php new file mode 100644 index 00000000..48b291db --- /dev/null +++ b/tests/Unit/Entities/DTO/MerchantBusinessEvent/CartInitiatedBusinessEventTest.php @@ -0,0 +1,42 @@ +assertEquals('cart_initiated', $event->getEventType()); + $this->assertEquals('54', $event->getCartId()); + } + + /** + * @dataProvider invalidDataForBusinessEventDataProvider + * @param $cartId + */ + public function testInvalidDataForBusinessEvent($cartId) + { + $this->expectException(ParametersException::class); + $this->expectExceptionMessage('CartId must be a string'); + new CartInitiatedBusinessEvent($cartId); + } + public static function invalidDataForBusinessEventDataProvider() + { + return [ + "cartId is an empty string" => [''], + "cartId is an int" => [1], + "cartId is a float" => [1.1], + "cartId is an array" => [[]], + "cartId is an object" => [new \stdClass()], + "cartId is a boolean" => [true], + "cartId is null" => [null], + ]; + } + +} diff --git a/tests/Unit/Entities/DTO/MerchantBusinessEvent/OrderConfirmedBusinessEventTest.php b/tests/Unit/Entities/DTO/MerchantBusinessEvent/OrderConfirmedBusinessEventTest.php new file mode 100644 index 00000000..935b34d6 --- /dev/null +++ b/tests/Unit/Entities/DTO/MerchantBusinessEvent/OrderConfirmedBusinessEventTest.php @@ -0,0 +1,276 @@ +assertEquals('order_confirmed', $event->getEventType()); + $this->assertEquals($isAlmaP1X, $event->isAlmaP1X()); + $this->assertEquals($isAlmaBNPL, $event->isAlmaBNPL()); + $this->assertEquals($wasBNPLEligible, $event->wasBNPLEligible()); + $this->assertEquals($orderId, $event->getOrderId()); + $this->assertEquals($cartId, $event->getCartId()); + $this->assertNull($event->getAlmaPaymentId()); + } + + /** + * @dataProvider invalidDataForBusinessEventDataProvider + * @param $isP1X + * @param $isBNPL + * @param $wasEligible + * @param $orderId + * @param $cartId + * @throws ParametersException + */ + public function testInvalidDataForBusinessEvent($isP1X, $isBNPL, $wasEligible, $orderId, $cartId) + { + $this->expectException(ParametersException::class); + new OrderConfirmedBusinessEvent($isP1X, $isBNPL, $wasEligible, $orderId, $cartId); + } + + public function testAlmaPaymentIdIsMandatoryForP1xAlmaPayment() + { + $this->expectException(ParametersException::class); + new OrderConfirmedBusinessEvent(true, false, true, "42", "54"); + } + + public function testAlmaPaymentIdIsMandatoryForBnplAlmaPayment() + { + $this->expectException(ParametersException::class); + new OrderConfirmedBusinessEvent(false, true, true, "42", "54"); + } + + public function testAlmaPaymentIdShouldBeAbsentForNonAlmaPayments() + { + $this->expectException(ParametersException::class); + new OrderConfirmedBusinessEvent( + false, + false, + true, + "42", + "54", + 'alma_payment_id' + ); + } + + public function testAlmaPaymentIdDataForAlmaPayment() + { + $data = [ + [ + 'isP1X' => true, + 'isBNPL' => false, + 'almaPaymentId' => 'almaPaymentId' + ], + [ + 'isP1X' => false, + 'isBNPL' => true, + 'almaPaymentId' => 'alma_payment_id' + ], + ]; + foreach ($data as $item) { + $orderConfirmedEvent = new OrderConfirmedBusinessEvent( + $item['isP1X'], + $item['isBNPL'], + true, + "42", + "54", + $item['almaPaymentId'] + ); + $this->assertEquals($item['almaPaymentId'], $orderConfirmedEvent->getAlmaPaymentId()); + } + } + + public static function invalidDataForBusinessEventDataProvider() + { + return [ + "isAlmaP1X is an int" => [ + 'isP1X' => 1, + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => "1", + 'cartId' => "1" + ], + "isAlmaP1X is an float" => [ + 'isP1X' => 1.1, + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => "1", + 'cartId' => "1" + ], + "isAlmaP1X is an array" => [ + 'isP1X' => [], + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => "1", + 'cartId' => "1" + ], + "isAlmaP1X is a string" => [ + 'isP1X' => "1", + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => "1", + 'cartId' => "1" + ], + "isAlmaP1X is a class" => [ + 'isP1X' => new \stdClass(), + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => "1", + 'cartId' => "1" + ], + "isBNPL is an int" => [ + 'isP1X' => false, + 'isBNPL' => 1, + 'wasEligible' => true, + 'orderId' => "1", + 'cartId' => "1" + ], + "isBNPL is an float" => [ + 'isP1X' => false, + 'isBNPL' => 1.1, + 'wasEligible' => true, + 'orderId' => "1", + 'cartId' => "1" + ], + "isBNPL is an array" => [ + 'isP1X' => false, + 'isBNPL' => [], + 'wasEligible' => true, + 'orderId' => "1", + 'cartId' => "1" + ], + "isBNPL is a string" => [ + 'isP1X' => false, + 'isBNPL' => '42', + 'wasEligible' => true, + 'orderId' => "1", + 'cartId' => "1" + ], + "isBNPL is a class" => [ + 'isP1X' => false, + 'isBNPL' => new \stdClass(), + 'wasEligible' => true, + 'orderId' => "1", + 'cartId' => "1" + ], + "was Eligible is an int" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => 1, + 'orderId' => "1", + 'cartId' => "1" + ], + "was Eligible is an float" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => 1.1, + 'orderId' => "1", + 'cartId' => "1" + ], + "was Eligible is an array" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => [], + 'orderId' => "1", + 'cartId' => "1" + ], + "was Eligible is a string" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => '45', + 'orderId' => "1", + 'cartId' => "1" + ], + "was Eligible is a class" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => new \stdClass(), + 'orderId' => "1", + 'cartId' => "1" + ], + "Order id is an int" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => 1, + 'cartId' => "1" + ], + "Order id is an float" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => 1.1, + 'cartId' => "1" + ], + "Order id is an array" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => [], + 'cartId' => "1" + ], + "Order id is a bool" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => true, + 'cartId' => "1" + ], + "Order id is a class" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => new \stdClass(), + 'cartId' => '1' + ], + "Cart id is an int" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => '1', + 'cartId' => 1 + ], + "Cart id is an float" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => '1', + 'cartId' => 1.1 + ], + "Cart id is an array" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => '1', + 'cartId' => [] + ], + "Cart id is a bool" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => '1', + 'cartId' => true + ], + "Cart id is a class" => [ + 'isP1X' => false, + 'isBNPL' => false, + 'wasEligible' => true, + 'orderId' => '1', + 'cartId' => new \stdClass() + ], + ]; + } +}