From a016726b5c9b5fecd725389b2e76fb9e5368cd2c Mon Sep 17 00:00:00 2001 From: "Adrian M." Date: Mon, 7 Jul 2025 19:38:38 -0500 Subject: [PATCH 1/4] Add support for Accept Hosted payment form and customer profile creation from transactions: - Introduced 'GetHostedPaymentPageRequest' to retrieve a token for launching the Accept Hosted payment form. - Added 'HostedPaymentSettings' and 'Setting' data types to manage hosted payment configuration settings. - Created 'CreateCustomerProfileFromTransactionRequest' to generate a customer profile, payment profile, and shipping address from an existing transaction. - Extended 'GetCustomerProfileRequest' with support for 'unmaskExpirationDate' flag. - Added comprehensive tests for: - 'HostedPaymentSettings' and 'Setting' functionality. - 'GetHostedPaymentPageRequest' with various configuration scenarios. - 'CreateCustomerProfileFromTransactionRequest' using both XML and JSON formats. - Minor update in 'CustomerProfileRequestTest' to explicitly set 'unmaskExpirationDate'. This commit enhances integration with Authorize.Net Accept Suite and customer profile APIs, enabling hosted form usage and simplifying profile management. --- ...eCustomerProfileFromTransactionRequest.php | 50 +++++++ src/DataTypes/HostedPaymentSettings.php | 20 +++ src/DataTypes/Setting.php | 37 +++++ src/GetCustomerProfileRequest.php | 14 +- src/GetHostedPaymentPageRequest.php | 91 ++++++++++++ ...tomerProfileFromTransactionRequestTest.php | 135 ++++++++++++++++++ tests/CustomerProfileRequestTest.php | 1 + tests/DataType/HostedPaymentSettingsTest.php | 70 +++++++++ tests/GetHostedPaymentPageRequestTest.php | 89 ++++++++++++ 9 files changed, 506 insertions(+), 1 deletion(-) create mode 100644 src/CreateCustomerProfileFromTransactionRequest.php create mode 100644 src/DataTypes/HostedPaymentSettings.php create mode 100644 src/DataTypes/Setting.php create mode 100644 src/GetHostedPaymentPageRequest.php create mode 100644 tests/CustomerProfileFromTransactionRequestTest.php create mode 100644 tests/DataType/HostedPaymentSettingsTest.php create mode 100644 tests/GetHostedPaymentPageRequestTest.php diff --git a/src/CreateCustomerProfileFromTransactionRequest.php b/src/CreateCustomerProfileFromTransactionRequest.php new file mode 100644 index 0000000..3e4856d --- /dev/null +++ b/src/CreateCustomerProfileFromTransactionRequest.php @@ -0,0 +1,50 @@ +transId = $transId; + } + + /** + * {@inheritdoc} + */ + protected function attachData(RequestInterface $request) + { + $request->addData('transId', $this->transId); + } +} diff --git a/src/DataTypes/HostedPaymentSettings.php b/src/DataTypes/HostedPaymentSettings.php new file mode 100644 index 0000000..a02713f --- /dev/null +++ b/src/DataTypes/HostedPaymentSettings.php @@ -0,0 +1,20 @@ +properties[] = ['setting' => $setting->toArray()]; + } +} diff --git a/src/DataTypes/Setting.php b/src/DataTypes/Setting.php new file mode 100644 index 0000000..5fdc528 --- /dev/null +++ b/src/DataTypes/Setting.php @@ -0,0 +1,37 @@ +properties['settingName'] = $name; + // Encode the value. + if (is_array($value)) { + $value = json_encode($value, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); + } + $this->properties['settingValue'] = $value; + } + +} diff --git a/src/GetCustomerProfileRequest.php b/src/GetCustomerProfileRequest.php index 91d4783..5faa48d 100644 --- a/src/GetCustomerProfileRequest.php +++ b/src/GetCustomerProfileRequest.php @@ -13,6 +13,7 @@ class GetCustomerProfileRequest extends BaseApiRequest { protected $customerProfileId; + protected $unmaskExpirationDate = false; public function __construct( Configuration $configuration, @@ -23,8 +24,19 @@ public function __construct( $this->customerProfileId = $customerProfileId; } - protected function attachData(RequestInterface $request) + /** + * @param boolean $unmaskExpirationDate + */ + public function setUnmaskExpirationDate($unmaskExpirationDate) + { + $this->unmaskExpirationDate = $unmaskExpirationDate; + } + + protected function attachData(RequestInterface $request) { $request->addData('customerProfileId', $this->customerProfileId); + if ($this->unmaskExpirationDate) { + $request->addData('unmaskExpirationDate', $this->unmaskExpirationDate); + } } } diff --git a/src/GetHostedPaymentPageRequest.php b/src/GetHostedPaymentPageRequest.php new file mode 100644 index 0000000..cf35a7b --- /dev/null +++ b/src/GetHostedPaymentPageRequest.php @@ -0,0 +1,91 @@ +transactionRequest = $transactionRequest; + $this->hostedPaymentSettings = $hostedPaymentSettings; + } + + /** + * {@inheritdoc} + */ + protected function attachData(RequestInterface $request) + { + // Attach transaction details. + if ($this->transactionRequest) { + $request->addDataType($this->transactionRequest); + } + // Add hosted payment settings. + if ($this->hostedPaymentSettings) { + $request->addDataType($this->hostedPaymentSettings); + } + } + + /** + * Sets the transaction request. + * + * @param \CommerceGuys\AuthNet\DataTypes\TransactionRequest $transactionRequest + * The transaction details. + * + * @return $this + */ + public function setTransactionRequest(TransactionRequest $transactionRequest) + { + $this->transactionRequest = $transactionRequest; + return $this; + } + + /** + * Sets the hosted payment settings. + * + * @param \CommerceGuys\AuthNet\DataTypes\HostedPaymentSettings $hostedPaymentSettings + * + * @return $this + */ + public function setHostedPaymentSettings(HostedPaymentSettings $hostedPaymentSettings) + { + $this->hostedPaymentSettings = $hostedPaymentSettings; + return $this; + } +} diff --git a/tests/CustomerProfileFromTransactionRequestTest.php b/tests/CustomerProfileFromTransactionRequestTest.php new file mode 100644 index 0000000..c12dde4 --- /dev/null +++ b/tests/CustomerProfileFromTransactionRequestTest.php @@ -0,0 +1,135 @@ +createChargableTransactionRequest(TransactionRequest::AUTH_ONLY); + $transactionRequest->addData('customer', [ + 'email' => 'xml_test@example.com', + ]); + $transactionRequest->addData('billTo', [ + 'firstName' => 'Jane', + 'lastName' => 'Doe', + 'address' => '456 Elm St', + 'city' => 'Springfield', + 'state' => 'IL', + 'zip' => '62704', + 'country' => 'US', + 'phoneNumber' => '5555555555', + ]); + + $transactionResponse = $this->xmlRequestFactory + ->createTransactionRequest() + ->setTransactionRequest($transactionRequest) + ->execute(); + + $this->assertEquals('Ok', $transactionResponse->getResultCode()); + $transId = $transactionResponse->transactionResponse->transId ?? null; + + $this->assertNotEmpty($transId, 'Transaction ID should not be empty'); + + $request = new CreateCustomerProfileFromTransactionRequest( + $this->configurationXml, + $this->client, + $transId + ); + /** @var \CommerceGuys\AuthNet\Response\XmlResponse $response */ + $response = $request->execute(); + $contents = $response->contents(); + + $this->assertEquals('Ok', $response->getResultCode()); + $this->assertEquals('I00001', $response->getMessages()[0]->getCode()); + $this->assertEquals('Successful.', $response->getMessages()[0]->getText()); + + $this->assertTrue(property_exists($contents, 'customerProfileId')); + $this->assertNotEmpty($contents->customerProfileId); + + $this->assertListFieldContainsData($contents, 'customerPaymentProfileIdList'); + $this->assertListFieldContainsData($contents, 'customerShippingAddressIdList'); + + $this->assertTrue(property_exists($contents, 'validationDirectResponseList')); + $this->assertInstanceOf(\stdClass::class, $contents->validationDirectResponseList); + } + + /** + * Tests the request with JSON format. + */ + public function testCreateCustomerProfileFromTransactionJson() + { + // Use a new number, otherwise a duplicate transaction is flagged. + $transactionRequest = $this->createChargableTransactionRequest( + TransactionRequest::AUTH_ONLY, + '4007000000027' + ); + $transactionRequest->addData('customer', [ + 'email' => 'json_test@example.com', + ]); + $transactionRequest->addData('billTo', [ + 'firstName' => 'John', + 'lastName' => 'Smith', + 'address' => '789 Oak St', + 'city' => 'Columbus', + 'state' => 'OH', + 'zip' => '43004', + 'country' => 'US', + 'phoneNumber' => '5555555555', + ]); + // Create the transaction. + $transactionResponse = $this->jsonRequestFactory + ->createTransactionRequest() + ->setTransactionRequest($transactionRequest) + ->execute(); + + $this->assertEquals('Ok', $transactionResponse->getResultCode()); + $transId = $transactionResponse->transactionResponse->transId ?? null; + + $this->assertNotEmpty($transId, 'Transaction ID should not be empty'); + + // Create the customer profile from the transaction. + $request = new CreateCustomerProfileFromTransactionRequest( + $this->configurationJson, + $this->client, + $transId + ); + /** @var \CommerceGuys\AuthNet\Response\JsonResponse $response */ + $response = $request->execute(); + $contents = $response->contents(); + + $this->assertEquals('Ok', $response->getResultCode()); + $this->assertEquals('I00001', $response->getMessages()[0]->getCode()); + $this->assertEquals('Successful.', $response->getMessages()[0]->getText()); + + $this->assertTrue(property_exists($contents, 'customerProfileId')); + $this->assertNotEmpty($contents->customerProfileId); + + $this->assertIsArray($contents->customerPaymentProfileIdList); + $this->assertNotEmpty($contents->customerPaymentProfileIdList); + + $this->assertIsArray($contents->customerShippingAddressIdList); + $this->assertNotEmpty($contents->customerShippingAddressIdList); + + $this->assertIsArray($contents->validationDirectResponseList); + } + + /** + * Helper for handling list fields that return stdClass with numericString in XML. + */ + protected function assertListFieldContainsData($object, string $fieldName): void + { + $this->assertTrue(property_exists($object, $fieldName)); + $list = (array) $object->{$fieldName}; + $this->assertArrayHasKey('numericString', $list, "$fieldName must contain numericString key"); + $this->assertNotEmpty($list['numericString'], "$fieldName.numericString must not be empty"); + } +} diff --git a/tests/CustomerProfileRequestTest.php b/tests/CustomerProfileRequestTest.php index d40b28f..93925e8 100644 --- a/tests/CustomerProfileRequestTest.php +++ b/tests/CustomerProfileRequestTest.php @@ -59,6 +59,7 @@ public function testCreateCustomerProfileCRUDRequests() $this->assertTrue(isset($response->validationDirectResponseList)); $request = new GetCustomerProfileRequest($this->configurationXml, $this->client, $response->customerProfileId); + $request->setUnmaskExpirationDate(false); $response = $request->execute(); $this->assertResponse($response, 'I00001', 'Successful.', 'Ok'); $this->assertTrue(isset($response->profile)); diff --git a/tests/DataType/HostedPaymentSettingsTest.php b/tests/DataType/HostedPaymentSettingsTest.php new file mode 100644 index 0000000..c4a4bcc --- /dev/null +++ b/tests/DataType/HostedPaymentSettingsTest.php @@ -0,0 +1,70 @@ + false, + 'url' => 'https://example.com/return', + ]; + $settings = new HostedPaymentSettings(); + $settings->addSetting(new Setting($settingName, $settingValue)); + + $array = $settings->toArray(); + + $this->assertCount(1, $array); + $this->assertArrayHasKey('setting', $array[0]); + $this->assertEquals('hostedPaymentReturnOptions', $array[0]['setting']['settingName']); + + $expectedValue = json_encode($settingValue); + $this->assertJsonStringEqualsJsonString($expectedValue, $array[0]['setting']['settingValue']); + } + + /** + * Tests adding multiple settings. + */ + public function testAddMultipleSettings() + { + $settings = new HostedPaymentSettings(); + + $settings->addSetting(new Setting('hostedPaymentBillingAddressOptions', ['show' => false])); + $settings->addSetting(new Setting('hostedPaymentSecurityOptions', ['captcha' => true])); + + $array = $settings->toArray(); + + $this->assertCount(2, $array); + + $this->assertEquals('hostedPaymentBillingAddressOptions', $array[0]['setting']['settingName']); + $this->assertEquals(json_encode(['show' => false]), $array[0]['setting']['settingValue']); + + $this->assertEquals('hostedPaymentSecurityOptions', $array[1]['setting']['settingName']); + $this->assertEquals(json_encode(['captcha' => true]), $array[1]['setting']['settingValue']); + } + + /** + * Tests that a scalar string value does not get double-encoded. + */ + public function testScalarSettingValue() + { + $settings = new HostedPaymentSettings(); + $settings->addSetting(new Setting('customLabel', 'Some Value')); + + $array = $settings->toArray(); + + $this->assertEquals('Some Value', $array[0]['setting']['settingValue']); + } +} diff --git a/tests/GetHostedPaymentPageRequestTest.php b/tests/GetHostedPaymentPageRequestTest.php new file mode 100644 index 0000000..3cf475b --- /dev/null +++ b/tests/GetHostedPaymentPageRequestTest.php @@ -0,0 +1,89 @@ + TransactionRequest::AUTH_ONLY, + 'amount' => '50.00', + ]); + // Minimal hosted payment settings. + $hostedSettings = new HostedPaymentSettings(); + $hostedSettings->addSetting(new Setting('hostedPaymentReturnOptions', [ + 'showReceipt' => true, + 'url' => 'https://mysite.com/receipt', + 'urlText' => 'Continue', + 'cancelUrl' => 'https://mysite.com/cancel', + 'cancelUrlText' => 'Cancel', + ])); + $request = new GetHostedPaymentPageRequest( + $this->configurationXml, + $this->client, + $transactionRequest, + $hostedSettings + ); + + /** @var \CommerceGuys\AuthNet\Response\XmlResponse $response */ + $response = $request->execute(); + $contents = $response->contents(); + + $this->assertEquals('Ok', $response->getResultCode()); + $this->assertEquals('I00001', $response->getMessages()[0]->getCode()); + $this->assertEquals('Successful.', $response->getMessages()[0]->getText()); + + $this->assertTrue(property_exists($contents, 'token')); + $this->assertNotEmpty($contents->token); + } + + /** + * Tests the Accept Hosted request with JSON format. + */ + public function testGetHostedPaymentPageJson() + { + // Minimal valid transaction request for Accept Hosted. + $transactionRequest = new TransactionRequest([ + 'transactionType' => TransactionRequest::AUTH_CAPTURE, + 'amount' => '20.00', + ]); + // Minimal hosted payment settings. + $hostedSettings = new HostedPaymentSettings(); + $hostedSettings->addSetting(new Setting('hostedPaymentReturnOptions', [ + 'showReceipt' => true, + 'url' => 'https://mysite.com/receipt', + 'urlText' => 'Continue', + 'cancelUrl' => 'https://mysite.com/cancel', + 'cancelUrlText' => 'Cancel' + ])); + + $request = new GetHostedPaymentPageRequest( + $this->configurationJson, + $this->client + ); + $request->setTransactionRequest($transactionRequest); + $request->setHostedPaymentSettings($hostedSettings); + + /** @var \CommerceGuys\AuthNet\Response\JsonResponse $response */ + $response = $request->execute(); + $contents = $response->contents(); + + $this->assertEquals('Ok', $response->getResultCode()); + $this->assertEquals('I00001', $response->getMessages()[0]->getCode()); + $this->assertEquals('Successful.', $response->getMessages()[0]->getText()); + + $this->assertTrue(property_exists($contents, 'token')); + $this->assertNotEmpty($contents->token); + } +} From 97af9f83e0daa7b688b746ff48ec83984d7f7cb7 Mon Sep 17 00:00:00 2001 From: "Adrian M." Date: Mon, 7 Jul 2025 20:03:41 -0500 Subject: [PATCH 2/4] Fix ARB subscription test failure due to past startDate: - Updated hardcoded 'startDate' in ARBCreateSubscriptionRequestTest to use a dynamic future date (1 day ahead) instead of a fixed date. - This resolves the API error: E00017: Start Date must not occur before the submission date. --- tests/ARBCreateSubscriptionRequestTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ARBCreateSubscriptionRequestTest.php b/tests/ARBCreateSubscriptionRequestTest.php index 5ced203..c1d6245 100644 --- a/tests/ARBCreateSubscriptionRequestTest.php +++ b/tests/ARBCreateSubscriptionRequestTest.php @@ -20,7 +20,7 @@ public function testARBCreateSubscriptionRequestCRUD() { $interval = new Interval(['length' => 7, 'unit' => 'days']); $paymentSchedule = new PaymentSchedule([ - 'startDate' => '2024-08-30', + 'startDate' => (new \DateTime('+1 day'))->format('Y-m-d'), 'totalOccurrences' => 9999, ]); $paymentSchedule->addInterval($interval); From 9a41157374cc90475065c62c2da2c270319c6525 Mon Sep 17 00:00:00 2001 From: "Adrian M." Date: Tue, 8 Jul 2025 17:53:17 -0500 Subject: [PATCH 3/4] Add AuthenticateTestRequest to validate API credentials - Introduced `AuthenticateTestRequest` class to support the `authenticateTestRequest` endpoint from Authorize.Net. - Allows verification of merchant API login and transaction key via the SDK. - Added `setMerchantAuthentication()` and `getMerchantAuthentication()` methods for credential injection. - Includes full test coverage with `AuthenticateTestRequestTest` for both XML and JSON request formats. --- src/AuthenticateTestRequest.php | 54 ++++++++++++++++++++++++++ tests/AuthenticateRequestTest.php | 63 +++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 src/AuthenticateTestRequest.php create mode 100644 tests/AuthenticateRequestTest.php diff --git a/src/AuthenticateTestRequest.php b/src/AuthenticateTestRequest.php new file mode 100644 index 0000000..63a023f --- /dev/null +++ b/src/AuthenticateTestRequest.php @@ -0,0 +1,54 @@ +merchantAuthentication = $merchantAuthentication; + return $this; + } + + /** + * Gets the merchant authentication credentials. + * + * @return \CommerceGuys\AuthNet\DataTypes\MerchantAuthentication|null + */ + public function getMerchantAuthentication(): ?MerchantAuthentication + { + return $this->merchantAuthentication; + } + + /** + * {@inheritdoc} + */ + protected function attachData(RequestInterface $request): void + { + // Only attach merchant authentication if available. + if ($this->merchantAuthentication) { + $request->addDataType($this->merchantAuthentication); + } + } +} diff --git a/tests/AuthenticateRequestTest.php b/tests/AuthenticateRequestTest.php new file mode 100644 index 0000000..a0fa46c --- /dev/null +++ b/tests/AuthenticateRequestTest.php @@ -0,0 +1,63 @@ +configurationXml, + $this->client + ); + + // Explicitly set authentication in case configuration does not preload it. + $request->setMerchantAuthentication(new MerchantAuthentication([ + 'name' => $this->configurationXml->getApiLogin(), + 'transactionKey' => $this->configurationXml->getTransactionKey(), + ])); + + /** @var \CommerceGuys\AuthNet\Response\XmlResponse $response */ + $response = $request->execute(); + + $this->assertEquals('Ok', $response->getResultCode()); + $this->assertEquals('I00001', $response->getMessages()[0]->getCode()); + $this->assertEquals('Successful.', $response->getMessages()[0]->getText()); + } + + /** + * Tests the authenticateTestRequest using JSON format. + */ + public function testAuthenticateTestJson(): void + { + $request = new AuthenticateTestRequest( + $this->configurationJson, + $this->client + ); + + $request->setMerchantAuthentication(new MerchantAuthentication([ + 'name' => $this->configurationJson->getApiLogin(), + 'transactionKey' => $this->configurationJson->getTransactionKey(), + ])); + + /** @var \CommerceGuys\AuthNet\Response\JsonResponse $response */ + $response = $request->execute(); + + $this->assertEquals('Ok', $response->getResultCode()); + $this->assertEquals('I00001', $response->getMessages()[0]->getCode()); + $this->assertEquals('Successful.', $response->getMessages()[0]->getText()); + } +} From 1c8fd458fcc76911286538ad8b3c96fdc059a6f1 Mon Sep 17 00:00:00 2001 From: "Adrian M." Date: Wed, 22 Oct 2025 08:18:38 -0500 Subject: [PATCH 4/4] Add method to remove settings from HostedPaymentSettings Introduces the removeSetting() method to allow removing a specific hosted payment configuration setting by name. Iterates through existing properties and unsets the matching entry based on 'settingName'. --- src/DataTypes/HostedPaymentSettings.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/DataTypes/HostedPaymentSettings.php b/src/DataTypes/HostedPaymentSettings.php index a02713f..04d55f6 100644 --- a/src/DataTypes/HostedPaymentSettings.php +++ b/src/DataTypes/HostedPaymentSettings.php @@ -17,4 +17,18 @@ public function addSetting(Setting $setting): void { $this->properties[] = ['setting' => $setting->toArray()]; } + + /** + * Removes setting from the hosted payment configuration. + * + * @param string $name + */ + public function removeSetting(string $name): void + { + foreach ($this->properties as $key => $property) { + if ($property['setting']['settingName'] == $name) { + unset($this->properties[$key]); + } + } + } }