diff --git a/CHANGELOG.md b/CHANGELOG.md index f334499f..fcbf7c65 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [3.11.0](https://github.com/unzerdev/php-sdk/compare/3.10.0..3.11.0) + +### Added + +* Add support for `preauthorize` transaction. + ## [3.10.0](https://github.com/unzerdev/php-sdk/compare/3.9.0..3.10.0) ### Added diff --git a/src/Constants/IdStrings.php b/src/Constants/IdStrings.php index f299f7d0..5ed9cc81 100755 --- a/src/Constants/IdStrings.php +++ b/src/Constants/IdStrings.php @@ -13,9 +13,10 @@ class IdStrings public const AUTHORIZE = 'aut'; public const CANCEL = 'cnl'; public const CHARGE = 'chg'; + public const CHARGEBACK = 'cbk'; public const PAYOUT = 'out'; + public const PREAUTHORIZE = 'preaut'; public const SHIPMENT = 'shp'; - public const CHARGEBACK = 'cbk'; // Payment Types public const ALIPAY = 'ali'; diff --git a/src/Constants/TransactionTypes.php b/src/Constants/TransactionTypes.php index 0a69608c..3a4721b1 100755 --- a/src/Constants/TransactionTypes.php +++ b/src/Constants/TransactionTypes.php @@ -11,6 +11,7 @@ class TransactionTypes { public const AUTHORIZATION = 'authorize'; + public const PREAUTHORIZATION = 'preauthorize'; public const CHARGE = 'charge'; public const REVERSAL = 'cancel-authorize'; public const REFUND = 'cancel-charge'; diff --git a/src/Constants/WebhookEvents.php b/src/Constants/WebhookEvents.php index 62faef4f..383569ed 100755 --- a/src/Constants/WebhookEvents.php +++ b/src/Constants/WebhookEvents.php @@ -22,6 +22,15 @@ class WebhookEvents public const AUTHORIZE_RESUMED = 'authorize.resumed'; public const AUTHORIZE_SUCCEEDED = 'authorize.succeeded'; + // preauthorize events + public const PREAUTHORIZE = 'preauthorize'; + public const PREAUTHORIZE_CANCELED = 'preauthorize.canceled'; + public const PREAUTHORIZE_EXPIRED = 'preauthorize.expired'; + public const PREAUTHORIZE_FAILED = 'preauthorize.failed'; + public const PREAUTHORIZE_PENDING = 'preauthorize.pending'; + public const PREAUTHORIZE_RESUMED = 'preauthorize.resumed'; + public const PREAUTHORIZE_SUCCEEDED = 'preauthorize.succeeded'; + // charge events public const CHARGE = 'charge'; public const CHARGE_CANCELED = 'charge.canceled'; @@ -69,6 +78,13 @@ class WebhookEvents self::AUTHORIZE_PENDING, self::AUTHORIZE_RESUMED, self::AUTHORIZE_SUCCEEDED, + self::PREAUTHORIZE, + self::PREAUTHORIZE_CANCELED, + self::PREAUTHORIZE_EXPIRED, + self::PREAUTHORIZE_FAILED, + self::PREAUTHORIZE_PENDING, + self::PREAUTHORIZE_RESUMED, + self::PREAUTHORIZE_SUCCEEDED, self::CHARGE, self::CHARGE_CANCELED, self::CHARGE_EXPIRED, diff --git a/src/Resources/Payment.php b/src/Resources/Payment.php index f91ae330..20f956d3 100755 --- a/src/Resources/Payment.php +++ b/src/Resources/Payment.php @@ -2,6 +2,8 @@ namespace UnzerSDK\Resources; +use RuntimeException; +use stdClass; use UnzerSDK\Adapter\HttpAdapterInterface; use UnzerSDK\Constants\CancelReasonCodes; use UnzerSDK\Constants\IdStrings; @@ -16,15 +18,13 @@ use UnzerSDK\Resources\TransactionTypes\Charge; use UnzerSDK\Resources\TransactionTypes\Chargeback; use UnzerSDK\Resources\TransactionTypes\Payout; +use UnzerSDK\Resources\TransactionTypes\PreAuthorization; use UnzerSDK\Resources\TransactionTypes\Shipment; use UnzerSDK\Services\IdService; use UnzerSDK\Traits\HasInvoiceId; use UnzerSDK\Traits\HasOrderId; use UnzerSDK\Traits\HasPaymentState; use UnzerSDK\Traits\HasTraceId; -use RuntimeException; -use stdClass; - use function is_string; /** @@ -903,6 +903,9 @@ private function updateResponseTransactions(array $transactions = []): void case TransactionTypes::AUTHORIZATION: $this->updateAuthorizationTransaction($transaction); break; + case TransactionTypes::PREAUTHORIZATION: + $this->updatePreAuthorizationTransaction($transaction); + break; case TransactionTypes::CHARGE: $this->updateChargeTransaction($transaction); break; @@ -994,6 +997,27 @@ private function updateAuthorizationTransaction(stdClass $transaction): void $authorization->handleResponse($transaction); } + /** + * This updates the local Authorization object referenced by this Payment with the given Authorization transaction + * from the Payment response. + * + * @param stdClass $transaction The transaction from the Payment response containing the Authorization data. + * + * @throws UnzerApiException An UnzerApiException is thrown if there is an error returned on API-request. + * @throws RuntimeException A RuntimeException is thrown when there is an error while using the SDK. + */ + private function updatePreAuthorizationTransaction(stdClass $transaction): void + { + $transactionId = IdService::getResourceIdFromUrl($transaction->url, IdStrings::PREAUTHORIZE); + $authorization = $this->getAuthorization(true); + if (!$authorization instanceof Authorization) { + $authorization = (new PreAuthorization())->setPayment($this)->setId($transactionId); + $this->setAuthorization($authorization); + } + + $authorization->handleResponse($transaction); + } + /** * This updates the local Charge object referenced by this Payment with the given Charge transaction from the * Payment response. diff --git a/src/Resources/TransactionTypes/PreAuthorization.php b/src/Resources/TransactionTypes/PreAuthorization.php new file mode 100755 index 00000000..1effcef9 --- /dev/null +++ b/src/Resources/TransactionTypes/PreAuthorization.php @@ -0,0 +1,22 @@ +fetchAuthorization(IdService::getResourceIdFromUrl($url, IdStrings::PAYMENT)); break; + case $resourceType === IdStrings::PREAUTHORIZE: + $resource = $unzer->fetchAuthorization(IdService::getResourceIdFromUrl($url, IdStrings::PAYMENT)); + break; case $resourceType === IdStrings::CHARGE: $resource = $unzer->fetchChargeById( IdService::getResourceIdFromUrl($url, IdStrings::PAYMENT), diff --git a/src/Unzer.php b/src/Unzer.php index c07d625b..733b7e31 100644 --- a/src/Unzer.php +++ b/src/Unzer.php @@ -57,7 +57,7 @@ class Unzer implements public const BASE_URL = 'api.unzer.com'; public const API_VERSION = 'v1'; public const SDK_TYPE = 'UnzerPHP'; - public const SDK_VERSION = '3.10.0'; + public const SDK_VERSION = '3.11.0'; /** @var string $key */ private $key; @@ -685,6 +685,9 @@ public function performAuthorization( return $this->paymentService->performAuthorization($authorization, $paymentType, $customer, $metadata, $basket); } + /** + * {@inheritDoc} + */ public function updateAuthorization($payment, Authorization $authorization): Authorization { return $this->paymentService->updateAuthorization($payment, $authorization); diff --git a/test/integration/TransactionTypes/PreAuthorizationTest.php b/test/integration/TransactionTypes/PreAuthorizationTest.php new file mode 100644 index 00000000..04dc6900 --- /dev/null +++ b/test/integration/TransactionTypes/PreAuthorizationTest.php @@ -0,0 +1,182 @@ +unzer->createPaymentType($this->createCardObject()); + $preauth = new PreAuthorization(100.0, 'EUR', self::RETURN_URL); + $this->unzer->performAuthorization($preauth, $paymentType->getId()); + $this->assertNotNull($preauth); + $this->assertNotEmpty($preauth->getId()); + $this->assertNotEmpty($preauth->getUniqueId()); + $this->assertNotEmpty($preauth->getShortId()); + + $traceId = $preauth->getTraceId(); + $this->assertNotEmpty($traceId); + $this->assertSame($traceId, $preauth->getPayment()->getTraceId()); + $this->assertPending($preauth); + } + + /** + * Verify authorization produces Payment and Customer. + * + * @test + */ + public function authorizationProducesPaymentAndCustomer(): void + { + $paymentType = $this->unzer->createPaymentType($this->createCardObject()); + + $customer = $this->getMinimalCustomer(); + $this->assertNull($customer->getId()); + + $preauth = new PreAuthorization(100.0, 'EUR', self::RETURN_URL); + $this->unzer->performAuthorization($preauth, $paymentType, $customer); + $payment = $preauth->getPayment(); + $this->assertNotNull($payment); + $this->assertNotNull($payment->getId()); + + $newCustomer = $payment->getCustomer(); + $this->assertNotNull($newCustomer); + $this->assertNotNull($newCustomer->getId()); + } + + /** + * Verify authorization with customer Id. + * + * @test + * + * @return Authorization + */ + public function authorizationWithCustomerId(): Authorization + { + $paymentType = $this->unzer->createPaymentType($this->createCardObject()); + + $customerId = $this->unzer->createCustomer($this->getMinimalCustomer())->getId(); + $orderId = microtime(true); + $preauth = (new PreAuthorization(100.0, 'EUR', self::RETURN_URL))->setOrderId($orderId); + $this->unzer->performAuthorization($preauth, $paymentType, $customerId); + $payment = $preauth->getPayment(); + $this->assertNotNull($payment); + $this->assertNotNull($payment->getId()); + + $newCustomer = $payment->getCustomer(); + $this->assertNotNull($newCustomer); + $this->assertNotNull($newCustomer->getId()); + + return $preauth; + } + + /** + * Verify authorization can be fetched. + * + * @depends authorizationWithCustomerId + * + * @test + * + * @param Authorization $authorization + */ + public function authorizationCanBeFetched(Authorization $authorization): void + { + $fetchedAuthorization = $this->unzer->fetchAuthorization($authorization->getPaymentId()); + $this->assertInstanceOf(PreAuthorization::class, $fetchedAuthorization); + $this->assertEquals($authorization->setCard3ds(true)->expose(), $fetchedAuthorization->expose()); + } + + + /** + * Verify authorize accepts all parameters. + * + * @test + */ + public function requestAuthorizationShouldAcceptAllParameters(): void + { + /** @var Card $card */ + $card = $this->unzer->createPaymentType($this->createCardObject()); + $customer = $this->getMinimalCustomer(); + $orderId = 'o' . self::generateRandomId(); + $metadata = (new Metadata())->addMetadata('key', 'value'); + $basket = $this->createBasket(); + $invoiceId = 'i' . self::generateRandomId(); + $paymentReference = 'paymentReference'; + + $preauth = new PreAuthorization(119.0, 'EUR', self::RETURN_URL); + $preauth->setRecurrenceType(RecurrenceTypes::ONE_CLICK, $card) + ->setOrderId($orderId) + ->setInvoiceId($invoiceId) + ->setPaymentReference($paymentReference); + + $preauth = $this->unzer->performAuthorization($preauth, $card, $customer, $metadata, $basket); + $payment = $preauth->getPayment(); + + $this->assertSame($card, $payment->getPaymentType()); + $this->assertEquals(119.0, $preauth->getAmount()); + $this->assertEquals('EUR', $preauth->getCurrency()); + $this->assertEquals(self::RETURN_URL, $preauth->getReturnUrl()); + $this->assertSame($customer, $payment->getCustomer()); + $this->assertEquals($orderId, $preauth->getOrderId()); + $this->assertSame($metadata, $payment->getMetadata()); + $this->assertSame($basket, $payment->getBasket()); + $this->assertTrue($preauth->isCard3ds()); + $this->assertEquals($invoiceId, $preauth->getInvoiceId()); + $this->assertEquals($paymentReference, $preauth->getPaymentReference()); + + $fetchedAuthorize = $this->unzer->fetchAuthorization($preauth->getPaymentId()); + $fetchedPayment = $fetchedAuthorize->getPayment(); + + $this->assertEquals($payment->getPaymentType()->expose(), $fetchedPayment->getPaymentType()->expose()); + $this->assertEquals($preauth->getAmount(), $fetchedAuthorize->getAmount()); + $this->assertEquals($preauth->getCurrency(), $fetchedAuthorize->getCurrency()); + $this->assertEquals($preauth->getReturnUrl(), $fetchedAuthorize->getReturnUrl()); + $this->assertEquals($payment->getCustomer()->expose(), $fetchedPayment->getCustomer()->expose()); + $this->assertEquals($preauth->getOrderId(), $fetchedAuthorize->getOrderId()); + $this->assertEquals($payment->getMetadata()->expose(), $fetchedPayment->getMetadata()->expose()); + $this->assertEquals($payment->getBasket()->expose(), $fetchedPayment->getBasket()->expose()); + $this->assertEquals($preauth->isCard3ds(), $fetchedAuthorize->isCard3ds()); + $this->assertEquals($preauth->getInvoiceId(), $fetchedAuthorize->getInvoiceId()); + $this->assertEquals($preauth->getPaymentReference(), $fetchedAuthorize->getPaymentReference()); + } + + // + + /** + * @return array + */ + public function authorizeHasExpectedStatesDP(): array + { + return [ + 'card' => [$this->createCardObject(), 'pending'], + 'paypal' => [new Paypal(), 'pending'] + ]; + } + + // +} diff --git a/test/integration/WebhookTest.php b/test/integration/WebhookTest.php index 5ea8e80d..beeb0f52 100755 --- a/test/integration/WebhookTest.php +++ b/test/integration/WebhookTest.php @@ -16,7 +16,6 @@ use UnzerSDK\Exceptions\UnzerApiException; use UnzerSDK\Resources\Webhook; use UnzerSDK\test\BaseIntegrationTest; - use function count; use function in_array; @@ -30,7 +29,7 @@ class WebhookTest extends BaseIntegrationTest * @test * * @dataProvider webhookResourceCanBeRegisteredAndFetchedDP - * + * @group CC-1576 * @param string $event */ public function webhookResourceCanBeRegisteredAndFetched($event): void @@ -51,7 +50,7 @@ public function webhookResourceCanBeRegisteredAndFetched($event): void */ public function webhookUrlShouldBeUpdateable(): void { - $url = $this->generateUniqueUrl(); + $url = $this->generateUniqueUrl(); $webhook = $this->unzer->createWebhook($url, WebhookEvents::ALL); $fetchedWebhook = $this->unzer->fetchWebhook($webhook->getId()); $this->assertEquals(WebhookEvents::ALL, $fetchedWebhook->getEvent()); @@ -123,6 +122,7 @@ public function webhookCreateShouldThrowErrorWhenEventIsAlreadyRegistered(): voi * Verify fetching all registered webhooks will return an array of webhooks. * * @test + * @group CC-1576 */ public function fetchWebhooksShouldReturnArrayOfRegisteredWebhooks(): void { @@ -139,14 +139,16 @@ public function fetchWebhooksShouldReturnArrayOfRegisteredWebhooks(): void $webhook1 = $this->unzer->createWebhook($this->generateUniqueUrl(), WebhookEvents::CUSTOMER); $webhook2 = $this->unzer->createWebhook($this->generateUniqueUrl(), WebhookEvents::CHARGE); $webhook3 = $this->unzer->createWebhook($this->generateUniqueUrl(), WebhookEvents::AUTHORIZE); + $webhook4 = $this->unzer->createWebhook($this->generateUniqueUrl(), WebhookEvents::PREAUTHORIZE); // --- Verify webhooks have been registered --- $fetchedWebhooks = $this->unzer->fetchAllWebhooks(); - $this->assertCount(3, $fetchedWebhooks); + $this->assertCount(4, $fetchedWebhooks); $this->assertTrue($this->arrayContainsWebhook($fetchedWebhooks, $webhook1)); $this->assertTrue($this->arrayContainsWebhook($fetchedWebhooks, $webhook2)); $this->assertTrue($this->arrayContainsWebhook($fetchedWebhooks, $webhook3)); + $this->assertTrue($this->arrayContainsWebhook($fetchedWebhooks, $webhook4)); } /** @@ -177,8 +179,8 @@ public function allWebhooksShouldBeRemovableAtOnce(): void */ public function bulkSettingWebhookEventsShouldBePossible(): void { - $webhookEvents = [WebhookEvents::AUTHORIZE, WebhookEvents::CHARGE, WebhookEvents::SHIPMENT]; - $url = $this->generateUniqueUrl(); + $webhookEvents = [WebhookEvents::AUTHORIZE, WebhookEvents::CHARGE, WebhookEvents::SHIPMENT, WebhookEvents::PREAUTHORIZE]; + $url = $this->generateUniqueUrl(); $registeredWebhooks = $this->unzer->registerMultipleWebhooks($url, $webhookEvents); // check whether the webhooks have the correct url @@ -207,7 +209,7 @@ public function bulkSettingOnlyOneWebhookShouldBePossible(): void // remove all existing webhooks a avoid errors here $this->unzer->deleteAllWebhooks(); - $url = $this->generateUniqueUrl(); + $url = $this->generateUniqueUrl(); $registeredWebhooks = $this->unzer->registerMultipleWebhooks($url, [WebhookEvents::AUTHORIZE]); $this->assertCount(1, $registeredWebhooks);