From 3a76b4f276c88fec3c2ea11b7817e305aeaa2595 Mon Sep 17 00:00:00 2001 From: "david.owusu" Date: Tue, 16 Jun 2026 14:22:21 +0200 Subject: [PATCH 1/4] [CC-2379] Fix fatal error when payment type resolves to null in performCharge and performSca. Co-Authored-By: Claude Sonnet 4.6 --- src/Services/PaymentService.php | 4 ++-- test/unit/Services/PaymentServiceTest.php | 29 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Services/PaymentService.php b/src/Services/PaymentService.php index e05dd4ccd..b84987d98 100755 --- a/src/Services/PaymentService.php +++ b/src/Services/PaymentService.php @@ -157,7 +157,7 @@ public function performCharge(Charge $charge, $paymentType, $customer = null, ?M $paymentType = $payment->getPaymentType(); /** @var Charge $charge */ - $charge->setSpecialParams($paymentType->getTransactionParams() ?? []); + $charge->setSpecialParams($paymentType?->getTransactionParams() ?? []); $payment->addCharge($charge)->setCustomer($customer)->setMetadata($metadata)->setBasket($basket); $this->getResourceService()->createResource($charge); @@ -417,7 +417,7 @@ public function performSca(Sca $sca, $paymentType, $customer = null, ?Metadata $ $paymentType = $payment->getPaymentType(); /** @var Sca $sca */ - $sca->setSpecialParams($paymentType->getTransactionParams() ?? []); + $sca->setSpecialParams($paymentType?->getTransactionParams() ?? []); $payment->setSca($sca)->setCustomer($customer)->setMetadata($metadata)->setBasket($basket); $this->getResourceService()->createResource($sca); diff --git a/test/unit/Services/PaymentServiceTest.php b/test/unit/Services/PaymentServiceTest.php index f73cb6c8f..2b898baef 100755 --- a/test/unit/Services/PaymentServiceTest.php +++ b/test/unit/Services/PaymentServiceTest.php @@ -31,6 +31,7 @@ use UnzerSDK\Resources\TransactionTypes\Cancellation; use UnzerSDK\Resources\TransactionTypes\Charge; use UnzerSDK\Resources\TransactionTypes\Payout; +use UnzerSDK\Resources\TransactionTypes\Sca; use UnzerSDK\Resources\TransactionTypes\Shipment; use UnzerSDK\Services\CancelService; use UnzerSDK\Services\PaymentService; @@ -283,6 +284,34 @@ public function chargePaymentShouldSetArgumentsInNewChargeObject(): void $this->assertEquals([$returnedCharge], $payment->getCharges()); } + /** + * Verify performCharge does not throw a fatal error when the payment type resolves to null. + * + * @test + */ + public function performChargeWithNullPaymentTypeShouldNotThrowFatalError(): void + { + $this->expectNotToPerformAssertions(); + $resourceSrvMock = $this->getMockBuilder(ResourceService::class)->disableOriginalConstructor()->setMethods(['createResource'])->getMock(); + $paymentSrv = (new Unzer('s-priv-123'))->setResourceService($resourceSrvMock)->getPaymentService(); + + $paymentSrv->performCharge(new Charge(), null); + } + + /** + * Verify performSca does not throw a fatal error when the payment type resolves to null. + * + * @test + */ + public function performScaWithNullPaymentTypeShouldNotThrowFatalError(): void + { + $this->expectNotToPerformAssertions(); + $resourceSrvMock = $this->getMockBuilder(ResourceService::class)->disableOriginalConstructor()->setMethods(['createResource'])->getMock(); + $paymentSrv = (new Unzer('s-priv-123'))->setResourceService($resourceSrvMock)->getPaymentService(); + + $paymentSrv->performSca(new Sca(), null); + } + // // From 3c9f6de57590c0068368ab4a599976b1f05da0e5 Mon Sep 17 00:00:00 2001 From: "david.owusu" Date: Tue, 16 Jun 2026 14:29:27 +0200 Subject: [PATCH 2/4] [CC-2379] Apply consistent nullsafe operator in performAuthorization. Co-Authored-By: Claude Sonnet 4.6 --- src/Services/PaymentService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/PaymentService.php b/src/Services/PaymentService.php index b84987d98..a8a11b2af 100755 --- a/src/Services/PaymentService.php +++ b/src/Services/PaymentService.php @@ -86,7 +86,7 @@ public function performAuthorization( ): Authorization { $payment = $this->createPayment($paymentType); $paymentType = $payment->getPaymentType(); - $authorization->setSpecialParams($paymentType !== null ? $paymentType->getTransactionParams() : []); + $authorization->setSpecialParams($paymentType?->getTransactionParams() ?? []); $payment->setAuthorization($authorization) ->setCustomer($customer) From 9d8d94e1023d31f89e615aae468a8e806bdeabae Mon Sep 17 00:00:00 2001 From: "david.owusu" Date: Wed, 17 Jun 2026 09:55:54 +0200 Subject: [PATCH 3/4] [CC-2379] Use explicit try/catch in null payment type tests for clearer intent. Co-Authored-By: Claude Sonnet 4.6 --- test/unit/Services/PaymentServiceTest.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/test/unit/Services/PaymentServiceTest.php b/test/unit/Services/PaymentServiceTest.php index 2b898baef..ea3441cc9 100755 --- a/test/unit/Services/PaymentServiceTest.php +++ b/test/unit/Services/PaymentServiceTest.php @@ -291,11 +291,15 @@ public function chargePaymentShouldSetArgumentsInNewChargeObject(): void */ public function performChargeWithNullPaymentTypeShouldNotThrowFatalError(): void { - $this->expectNotToPerformAssertions(); $resourceSrvMock = $this->getMockBuilder(ResourceService::class)->disableOriginalConstructor()->setMethods(['createResource'])->getMock(); $paymentSrv = (new Unzer('s-priv-123'))->setResourceService($resourceSrvMock)->getPaymentService(); - $paymentSrv->performCharge(new Charge(), null); + try { + $paymentSrv->performCharge(new Charge(), null); + $this->assertTrue(true); + } catch (\Throwable $e) { + $this->fail('performCharge threw an unexpected error: ' . $e->getMessage()); + } } /** @@ -305,11 +309,15 @@ public function performChargeWithNullPaymentTypeShouldNotThrowFatalError(): void */ public function performScaWithNullPaymentTypeShouldNotThrowFatalError(): void { - $this->expectNotToPerformAssertions(); $resourceSrvMock = $this->getMockBuilder(ResourceService::class)->disableOriginalConstructor()->setMethods(['createResource'])->getMock(); $paymentSrv = (new Unzer('s-priv-123'))->setResourceService($resourceSrvMock)->getPaymentService(); - $paymentSrv->performSca(new Sca(), null); + try { + $paymentSrv->performSca(new Sca(), null); + $this->assertTrue(true); + } catch (\Throwable $e) { + $this->fail('performSca threw an unexpected error: ' . $e->getMessage()); + } } // From 105640d122bb833d5f05b30a18205a76a32c775e Mon Sep 17 00:00:00 2001 From: "david.owusu" Date: Thu, 18 Jun 2026 17:39:25 +0200 Subject: [PATCH 4/4] [CC-3759] Implement JsonSerializable interface on AbstractUnzerResource and ApplepaySession. jsonSerialize() now returns expose() data instead of a pre-encoded string, making json_encode($resource) work correctly for all resources. Co-Authored-By: Claude Sonnet 4.6 --- src/Adapter/ApplepayAdapter.php | 2 +- src/Resources/AbstractUnzerResource.php | 13 ++++--------- src/Resources/ExternalResources/ApplepaySession.php | 11 ++++------- src/Services/HttpService.php | 2 +- test/unit/Adapter/ApplepaySessionTest.php | 2 +- test/unit/DummyResource.php | 4 ++-- test/unit/Resources/AbstractUnzerResourceTest.php | 10 +++++----- test/unit/Resources/PaymentTypes/ApplePayTest.php | 2 +- test/unit/Resources/PaymentTypes/ClickToPayTest.php | 2 +- test/unit/Resources/PaymentTypes/GooglePayTest.php | 2 +- .../unit/Resources/PaymentTypes/OpenBankingTest.php | 2 +- test/unit/Services/HttpServiceTest.php | 4 ++-- test/unit/Traits/HasRecurrenceTypeTest.php | 4 ++-- 13 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/Adapter/ApplepayAdapter.php b/src/Adapter/ApplepayAdapter.php index 820655cf9..3738586b0 100644 --- a/src/Adapter/ApplepayAdapter.php +++ b/src/Adapter/ApplepayAdapter.php @@ -37,7 +37,7 @@ public function validateApplePayMerchant( if ($this->request === null) { throw new ApplepayMerchantValidationException('No curl adapter initiated yet. Make sure to cal init() function before.'); } - $payload = $applePaySession->jsonSerialize(); + $payload = json_encode($applePaySession, JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION); $this->setOption(CURLOPT_URL, $merchantValidationURL); $this->setOption(CURLOPT_POSTFIELDS, $payload); diff --git a/src/Resources/AbstractUnzerResource.php b/src/Resources/AbstractUnzerResource.php index 59a3750d7..f3adf7bae 100644 --- a/src/Resources/AbstractUnzerResource.php +++ b/src/Resources/AbstractUnzerResource.php @@ -36,7 +36,7 @@ * @link https://docs.unzer.com/ * */ -abstract class AbstractUnzerResource implements UnzerParentInterface +abstract class AbstractUnzerResource implements UnzerParentInterface, \JsonSerializable { /** @var string $id */ protected $id; @@ -332,16 +332,11 @@ protected function fetchResource(AbstractUnzerResource $resource): void } /** - * Specify data which should be serialized to JSON - * - * @link http://php.net/manual/en/jsonserializable.jsonserialize.php - * - * @return false|string data which can be serialized by json_encode, - * which is a value of any type other than a resource. + * {@inheritDoc} */ - public function jsonSerialize() + public function jsonSerialize(): mixed { - return json_encode($this->expose(), JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION); + return $this->expose(); } /** diff --git a/src/Resources/ExternalResources/ApplepaySession.php b/src/Resources/ExternalResources/ApplepaySession.php index 2f3ebc7c4..29fa23e17 100644 --- a/src/Resources/ExternalResources/ApplepaySession.php +++ b/src/Resources/ExternalResources/ApplepaySession.php @@ -8,7 +8,7 @@ * @link https://docs.unzer.com/ * */ -class ApplepaySession +class ApplepaySession implements \JsonSerializable { /** * This can be found in the Apple Developer Account @@ -46,14 +46,11 @@ public function __construct(string $merchantIdentifier, string $displayName, str } /** - * Returns the json representation of this object's properties. - * - * @return false|string + * {@inheritDoc} */ - public function jsonSerialize() + public function jsonSerialize(): mixed { - $properties = get_object_vars($this); - return json_encode($properties, JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION); + return get_object_vars($this); } /** diff --git a/src/Services/HttpService.php b/src/Services/HttpService.php index 68bcdc0c7..f341e86ac 100755 --- a/src/Services/HttpService.php +++ b/src/Services/HttpService.php @@ -128,7 +128,7 @@ public function sendRequest(ApiRequest $request): string // perform request $requestUrl = $this->buildRequestUrl($request); - $payload = $request->getResource()->jsonSerialize(); + $payload = json_encode($request->getResource(), JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION); $headers = $this->composeHttpHeaders($unzerObj, $apiConfig::getAuthorizationMethod()); $httpMethod = $request->getHttpMethod(); $this->initRequest($requestUrl, $payload, $httpMethod, $headers); diff --git a/test/unit/Adapter/ApplepaySessionTest.php b/test/unit/Adapter/ApplepaySessionTest.php index d2a225852..b9a1b9fe8 100644 --- a/test/unit/Adapter/ApplepaySessionTest.php +++ b/test/unit/Adapter/ApplepaySessionTest.php @@ -18,7 +18,7 @@ public function testJsonSerialize(): void $applepaySession = new ApplepaySession('merchantIdentifier', 'displayName', 'domainName'); $expectedJson = '{"merchantIdentifier": "merchantIdentifier", "displayName": "displayName", "domainName": "domainName"}'; - $jsonSerialize = $applepaySession->jsonSerialize(); + $jsonSerialize = json_encode($applepaySession); $this->assertJsonStringEqualsJsonString($expectedJson, $jsonSerialize); } } diff --git a/test/unit/DummyResource.php b/test/unit/DummyResource.php index 2a7f34c7a..717308e88 100755 --- a/test/unit/DummyResource.php +++ b/test/unit/DummyResource.php @@ -43,9 +43,9 @@ public function setTestFloat(float $testFloat): DummyResource // // - public function jsonSerialize() + public function jsonSerialize(): mixed { - return '{"dummyResource": "JsonSerialized"}'; + return ['dummyResource' => 'JsonSerialized']; } public function getUri(bool $appendId = true, string $httpMethod = HttpAdapterInterface::REQUEST_GET): string diff --git a/test/unit/Resources/AbstractUnzerResourceTest.php b/test/unit/Resources/AbstractUnzerResourceTest.php index 5e6b0194c..e485895b0 100644 --- a/test/unit/Resources/AbstractUnzerResourceTest.php +++ b/test/unit/Resources/AbstractUnzerResourceTest.php @@ -239,8 +239,8 @@ public function updateValuesShouldUpdateChildObjects(): void ->setCommercialSector(CompanyCommercialSectorItems::AIR_TRANSPORT); $testResponse = new stdClass(); - $testResponse->billingAddress = json_decode($address->jsonSerialize(), false); - $testResponse->companyInfo = json_decode($info->jsonSerialize(), false); + $testResponse->billingAddress = json_decode(json_encode($address), false); + $testResponse->companyInfo = json_decode(json_encode($info), false); $customer = new Customer(); $customer->handleResponse($testResponse); @@ -319,7 +319,7 @@ public function jsonSerializeShouldTranslateResourceIntoJson(): void '"firstname":"Peter","lastname":"Universum","mobile":"+49172123456","param1":"value1","param2":"value2",' . '"phone":"+4962216471100","salutation":"mr","shippingAddress":{"city":"Frankfurt am Main","country":"DE",' . '"name":"Peter Universum","state":"DE-BO","street":"Hugo-Junkers-Str. 5","zip":"60386"}}'; - $this->assertEquals($expectedJson, $customer->jsonSerialize()); + $this->assertJsonStringEqualsJsonString($expectedJson, json_encode($customer)); } /** @@ -423,8 +423,8 @@ public function parentPrivatePropertiesShouldNotCauseAnException(): void $metadata->setParentResource(new Unzer('s-priv-123')); $metadata->setSpecialParams(['something' => 'special']); - $result = $metadata->jsonSerialize(); - $this->assertEquals('{"something":"special"}', $result); + $result = json_encode($metadata); + $this->assertJsonStringEqualsJsonString('{"something":"special"}', $result); } // diff --git a/test/unit/Resources/PaymentTypes/ApplePayTest.php b/test/unit/Resources/PaymentTypes/ApplePayTest.php index 7d33e684e..f4e58fb15 100644 --- a/test/unit/Resources/PaymentTypes/ApplePayTest.php +++ b/test/unit/Resources/PaymentTypes/ApplePayTest.php @@ -47,7 +47,7 @@ public function jsonSerializationExposesOnlyRequestParameter(): void $expectedJson = '{ "data": "data", "header": { "ephemeralPublicKey": "ephemeralPublicKey", "publicKeyHash": ' . '"publicKeyHash", "transactionId": "transactionId" }, "signature": "sig", "version": "EC_v1" }'; - $this->assertJsonStringEqualsJsonString($expectedJson, $applepay->jsonSerialize()); + $this->assertJsonStringEqualsJsonString($expectedJson, json_encode($applepay)); } /** diff --git a/test/unit/Resources/PaymentTypes/ClickToPayTest.php b/test/unit/Resources/PaymentTypes/ClickToPayTest.php index 943726a29..265a7b185 100644 --- a/test/unit/Resources/PaymentTypes/ClickToPayTest.php +++ b/test/unit/Resources/PaymentTypes/ClickToPayTest.php @@ -44,7 +44,7 @@ public function jsonSerialization(): void ); $expectedJson = JsonProvider::getJsonFromFile('clicktopay/createRequest.json'); - $this->assertJsonStringEqualsJsonString($expectedJson, $clickToPayObject->jsonSerialize()); + $this->assertJsonStringEqualsJsonString($expectedJson, json_encode($clickToPayObject)); } /** diff --git a/test/unit/Resources/PaymentTypes/GooglePayTest.php b/test/unit/Resources/PaymentTypes/GooglePayTest.php index 80559db3a..304a4a4cf 100644 --- a/test/unit/Resources/PaymentTypes/GooglePayTest.php +++ b/test/unit/Resources/PaymentTypes/GooglePayTest.php @@ -46,7 +46,7 @@ public function jsonSerializationExposesOnlyRequestParameter(): void $googlepay = $this->getTestGooglepay(); $expectedJson = JsonProvider::getJsonFromFile('googlePay/createRequest.json'); - $this->assertJsonStringEqualsJsonString($expectedJson, $googlepay->jsonSerialize()); + $this->assertJsonStringEqualsJsonString($expectedJson, json_encode($googlepay)); } /** diff --git a/test/unit/Resources/PaymentTypes/OpenBankingTest.php b/test/unit/Resources/PaymentTypes/OpenBankingTest.php index 765d40d8e..5addf7d24 100644 --- a/test/unit/Resources/PaymentTypes/OpenBankingTest.php +++ b/test/unit/Resources/PaymentTypes/OpenBankingTest.php @@ -33,7 +33,7 @@ public function jsonSerialization(): void $openBankingObject = new OpenbankingPis("DE",); $expectedJson = JsonProvider::getJsonFromFile('openBanking/createRequest.json'); - $this->assertJsonStringEqualsJsonString($expectedJson, $openBankingObject->jsonSerialize()); + $this->assertJsonStringEqualsJsonString($expectedJson, json_encode($openBankingObject)); } /** diff --git a/test/unit/Services/HttpServiceTest.php b/test/unit/Services/HttpServiceTest.php index 816b43fe8..f2e60cd91 100755 --- a/test/unit/Services/HttpServiceTest.php +++ b/test/unit/Services/HttpServiceTest.php @@ -97,7 +97,7 @@ static function ($url) { return str_replace(['dev-api', 'stg-api'], 'sbx-api', $url) === 'https://sbx-api.unzer.com/v1/my/uri/123'; } ), - '{"dummyResource": "JsonSerialized"}', + '{"dummyResource":"JsonSerialized"}', 'GET' ); /** @noinspection PhpParamsInspection */ @@ -223,7 +223,7 @@ static function ($string) { } ) ], - ['(' . (getmypid()) . ') Request: {"dummyResource": "JsonSerialized"}'], + ['(' . (getmypid()) . ') Request: {"dummyResource":"JsonSerialized"}'], ['(' . (getmypid()) . ') Response: (201) {"response":"myResponseString"}'] ); diff --git a/test/unit/Traits/HasRecurrenceTypeTest.php b/test/unit/Traits/HasRecurrenceTypeTest.php index f2044f275..36a0539d9 100644 --- a/test/unit/Traits/HasRecurrenceTypeTest.php +++ b/test/unit/Traits/HasRecurrenceTypeTest.php @@ -76,7 +76,7 @@ public function recurrenceTypeShouldBeExposedProperly(): void $this->assertEquals('oneclick', $exposedTransaction['additionalTransactionData']->card['recurrenceType']); $this->assertStringContainsString( '"additionalTransactionData":{"card":{"recurrenceType":"oneclick"}}', - $charge->jsonSerialize() + json_encode($charge) ); } @@ -108,7 +108,7 @@ public function responseShouldBeHandledProperlyWithRecurrenceType(): void $this->assertEquals('oneclick', $charge->getRecurrenceType()); $this->assertStringContainsString( '"additionalTransactionData":{"card":{"recurrenceType":"oneclick"}}', - $charge->jsonSerialize() + json_encode($charge) ); }