From 96ea89d0f9378ca20b39903978614f1b8b8a3396 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 7 Oct 2026 16:02:28 +0200 Subject: [PATCH 1/3] CashApp functionality --- .../Setups/Common/Customer/Customer.php | 33 ++ .../Setups/Common/Customer/Device.php | 39 ++ .../Setups/Common/Customer/DeviceClient.php | 13 + .../Setups/Common/Customer/DeviceOs.php | 12 + .../Common/PaymentMethods/CashApp/CashApp.php | 49 +++ .../PaymentMethods/CashApp/CashAppAction.php | 27 ++ .../CashApp/CashAppActionType.php | 11 + .../PaymentMethods/CashApp/CashAppAddress.php | 77 ++++ .../CashApp/CashAppCustomerProfile.php | 119 ++++++ .../Common/PaymentMethodBase.php | 12 + .../Common/PaymentMethods/PaymentMethods.php | 8 + .../PaymentSetupFieldsSerializationTest.php | 401 ++++++++++++++++++ .../Setups/PaymentSetupsClientTest.php | 22 + .../Setups/PaymentSetupsIntegrationTest.php | 80 ++++ 14 files changed, 903 insertions(+) create mode 100644 lib/Checkout/Payments/Setups/Common/Customer/DeviceClient.php create mode 100644 lib/Checkout/Payments/Setups/Common/Customer/DeviceOs.php create mode 100644 lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashApp.php create mode 100644 lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashAppAction.php create mode 100644 lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashAppActionType.php create mode 100644 lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashAppAddress.php create mode 100644 lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashAppCustomerProfile.php diff --git a/lib/Checkout/Payments/Setups/Common/Customer/Customer.php b/lib/Checkout/Payments/Setups/Common/Customer/Customer.php index 3a36a590..84f43ea7 100644 --- a/lib/Checkout/Payments/Setups/Common/Customer/Customer.php +++ b/lib/Checkout/Payments/Setups/Common/Customer/Customer.php @@ -7,27 +7,60 @@ class Customer { /** + * Details of the customer's email. + * [Optional] * @var Email */ public $email; /** + * The customer's full name. + * [Optional] + * max 100 characters * @var string */ public $name; /** + * The customer's phone number. + * [Optional] * @var Phone */ public $phone; /** + * Details of the customer's device. + * [Optional] * @var Device */ public $device; /** + * Details of the account the customer holds with the merchant. + * [Optional] * @var MerchantAccount */ public $merchant_account; + + /** + * The unique identifier of the customer. + * [Optional] + * @var string + */ + public $id; + + /** + * The two-letter ISO country code of the customer for this payment. + * [Optional] + * min 2 characters, max 2 characters + * @var string value of Country + */ + public $country; + + /** + * The customer's tax identification number. + * [Optional] + * @var string + */ + public $tax_number; } diff --git a/lib/Checkout/Payments/Setups/Common/Customer/Device.php b/lib/Checkout/Payments/Setups/Common/Customer/Device.php index 5bbe5fb1..2953dda4 100644 --- a/lib/Checkout/Payments/Setups/Common/Customer/Device.php +++ b/lib/Checkout/Payments/Setups/Common/Customer/Device.php @@ -5,7 +5,46 @@ class Device { /** + * The locale of the device. + * [Optional] * @var string */ public $locale; + + /** + * A unique identifier for the customer's device. + * [Optional] + * @var string + */ + public $fingerprint; + + /** + * The customer's device IPv4 address, used by some payment methods for risk and eligibility checks. + * [Optional] + * @var string + */ + public $ipv4; + + /** + * The customer's device IPv6 address, used by some payment methods for risk and eligibility checks. + * [Optional] + * @var string + */ + public $ipv6; + + /** + * The type of client the customer uses to initiate the payment. Required when using Cash App Pay. + * [Optional] + * Enum: "web" "mobile_web" "app" + * @var string value of DeviceClient + */ + public $client; + + /** + * The operating system of the customer's device. + * [Optional] + * Enum: "android" "ios" + * @var string value of DeviceOs + */ + public $os; } diff --git a/lib/Checkout/Payments/Setups/Common/Customer/DeviceClient.php b/lib/Checkout/Payments/Setups/Common/Customer/DeviceClient.php new file mode 100644 index 00000000..76c5cede --- /dev/null +++ b/lib/Checkout/Payments/Setups/Common/Customer/DeviceClient.php @@ -0,0 +1,13 @@ +assertArrayNotHasKey('presentment_details', $decoded); $this->assertArrayNotHasKey('terminal', $decoded); } + + public function testSerializesCashAppUnderLiteralKeyWithMerchantFields() + { + $cashApp = new CashApp(); + $cashApp->initialization = "enabled"; + $cashApp->customer_profile_sharing = true; + + $paymentMethods = new PaymentMethods(); + $paymentMethods->cashapp = $cashApp; + + $request = new PaymentSetupRequest(); + $request->processing_channel_id = "pc_aaaaaaaaaaaaaaaaaaaaaaaaaa"; + $request->amount = 1000; + $request->currency = "USD"; + $request->payment_methods = $paymentMethods; + + $json = (new JsonSerializer())->serialize($request); + $decoded = json_decode($json, true); + + $this->assertStringContainsString('"cashapp":', $json); + $this->assertStringNotContainsString('cash_app', $json); + $this->assertStringNotContainsString('cashApp', $json); + $this->assertStringNotContainsString('customerProfileSharing', $json); + $this->assertSame( + ["initialization" => "enabled", "customer_profile_sharing" => true], + $decoded['payment_methods']['cashapp'] + ); + } + + public function testSerializesEveryDeviceClientValue() + { + $serializer = new JsonSerializer(); + $device = new Device(); + + $device->client = DeviceClient::$web; + $this->assertSame('{"client":"web"}', $serializer->serialize($device)); + + $device->client = DeviceClient::$mobile_web; + $this->assertSame('{"client":"mobile_web"}', $serializer->serialize($device)); + + $device->client = DeviceClient::$app; + $this->assertSame('{"client":"app"}', $serializer->serialize($device)); + } + + public function testSerializesEveryDeviceOsValue() + { + $serializer = new JsonSerializer(); + $device = new Device(); + + $device->os = DeviceOs::$android; + $this->assertSame('{"os":"android"}', $serializer->serialize($device)); + + $device->os = DeviceOs::$ios; + $this->assertSame('{"os":"ios"}', $serializer->serialize($device)); + } + + public function testSerializesAllDeviceFields() + { + $device = new Device(); + $device->locale = "en_US"; + $device->fingerprint = "fp_abc123xyz"; + $device->ipv4 = "203.0.113.0"; + $device->ipv6 = "2001:db8:85a3::8a2e:370:7334"; + $device->client = DeviceClient::$web; + $device->os = DeviceOs::$android; + + $customer = new Customer(); + $customer->device = $device; + $request = new PaymentSetupRequest(); + $request->customer = $customer; + + $decoded = json_decode((new JsonSerializer())->serialize($request), true); + + $this->assertSame( + [ + "locale" => "en_US", + "fingerprint" => "fp_abc123xyz", + "ipv4" => "203.0.113.0", + "ipv6" => "2001:db8:85a3::8a2e:370:7334", + "client" => "web", + "os" => "android", + ], + $decoded['customer']['device'] + ); + } + + public function testSerializesDeviceWithOnlyLocale() + { + $device = new Device(); + $device->locale = "en_US"; + + $this->assertSame('{"locale":"en_US"}', (new JsonSerializer())->serialize($device)); + } + + public function testCashAppRoundTripKeepsEveryProperty() + { + $address = new CashAppAddress(); + $address->address_line_1 = "123 Main St"; + $address->address_line_2 = "Apt 2"; + $address->address_line_3 = "Floor 3"; + $address->locality = "Springfield"; + $address->sublocality = "Downtown"; + $address->administrative_district_level_1 = "IL"; + $address->postal_code = "62701"; + $address->country = "US"; + + $profile = new CashAppCustomerProfile(); + $profile->customer_id = self::CASHAPP_CUSTOMER_ID; + $profile->cashtag = '$CASHTAG_C_TOKEN'; + $profile->reference_id = "value"; + $profile->full_name = "John Middle Doe"; + $profile->given_name = "John"; + $profile->middle_name = "Middle"; + $profile->family_name = "Doe"; + $profile->suffix = "Jr."; + $profile->birth_date = "1990-01-01T00:00:00.0000000"; + $profile->address = $address; + $profile->phone_number = "5555555555"; + $profile->email_address = "cash@cash.com"; + $profile->customer_since = "1970-01-18T12:46:04.8000000+00:00"; + + $action = new CashAppAction(); + $action->type = CashAppActionType::$redirect; + $action->redirect_url = self::CASHAPP_REDIRECT_URL; + + $cashApp = new CashApp(); + $cashApp->status = "action_required"; + $cashApp->flags = []; + $cashApp->initialization = "enabled"; + $cashApp->customer_profile_sharing = true; + $cashApp->reference = "ORDER-99"; + $cashApp->action = $action; + $cashApp->customer_profile = $profile; + + $serializer = new JsonSerializer(); + $json = $serializer->serialize($cashApp); + $decoded = $serializer->deserialize($json); + + $this->assertStringContainsString('"address_line_1":', $json); + $this->assertStringContainsString('"address_line_2":', $json); + $this->assertStringContainsString('"address_line_3":', $json); + $this->assertStringContainsString('"administrative_district_level_1":', $json); + $this->assertStringNotContainsString('"address_line1"', $json); + + $this->assertSame("action_required", $decoded['status']); + $this->assertSame([], $decoded['flags']); + $this->assertSame("enabled", $decoded['initialization']); + $this->assertTrue($decoded['customer_profile_sharing']); + $this->assertSame("ORDER-99", $decoded['reference']); + $this->assertSame("redirect", $decoded['action']['type']); + $this->assertSame(self::CASHAPP_REDIRECT_URL, $decoded['action']['redirect_url']); + + $decodedProfile = $decoded['customer_profile']; + $this->assertCount(13, $decodedProfile); + $this->assertSame(self::CASHAPP_CUSTOMER_ID, $decodedProfile['customer_id']); + $this->assertSame('$CASHTAG_C_TOKEN', $decodedProfile['cashtag']); + $this->assertSame("value", $decodedProfile['reference_id']); + $this->assertSame("John Middle Doe", $decodedProfile['full_name']); + $this->assertSame("John", $decodedProfile['given_name']); + $this->assertSame("Middle", $decodedProfile['middle_name']); + $this->assertSame("Doe", $decodedProfile['family_name']); + $this->assertSame("Jr.", $decodedProfile['suffix']); + $this->assertSame("1990-01-01T00:00:00.0000000", $decodedProfile['birth_date']); + $this->assertSame("5555555555", $decodedProfile['phone_number']); + $this->assertSame("cash@cash.com", $decodedProfile['email_address']); + $this->assertSame("1970-01-18T12:46:04.8000000+00:00", $decodedProfile['customer_since']); + $this->assertSame( + [ + "address_line_1" => "123 Main St", + "address_line_2" => "Apt 2", + "address_line_3" => "Floor 3", + "locality" => "Springfield", + "sublocality" => "Downtown", + "administrative_district_level_1" => "IL", + "postal_code" => "62701", + "country" => "US", + ], + $decodedProfile['address'] + ); + } + + public function testDeserializesCashAppSwaggerExampleResponse() + { + $json = '{ + "id": "ps_2Un4Ld0Qm8Dj6kXHm0LqT0bK5mL", + "payment_methods": { + "cashapp": { + "status": "action_required", + "flags": [], + "initialization": "enabled", + "customer_profile_sharing": true, + "reference": "ORDER-99", + "action": { + "type": "redirect", + "redirect_url": "' . self::CASHAPP_REDIRECT_URL . '" + }, + "customer_profile": { + "customer_id": "' . self::CASHAPP_CUSTOMER_ID . '", + "cashtag": "$CASHTAG_C_TOKEN", + "reference_id": "value", + "full_name": "John Middle Doe", + "given_name": "John", + "middle_name": "Middle", + "family_name": "Doe", + "suffix": "Jr.", + "birth_date": "1990-01-01T00:00:00.0000000", + "address": { + "address_line_1": "123 Main St", + "address_line_2": "Apt 2", + "address_line_3": "Floor 3", + "locality": "Springfield", + "sublocality": "Downtown", + "administrative_district_level_1": "IL", + "postal_code": "62701", + "country": "US" + }, + "phone_number": "5555555555", + "email_address": "cash@cash.com", + "customer_since": "1970-01-18T12:46:04.8000000+00:00" + } + } + }, + "customer": { + "device": { + "locale": "en_US", + "fingerprint": "fp_abc123xyz", + "ipv4": "203.0.113.0", + "ipv6": "2001:db8:85a3::8a2e:370:7334", + "client": "web", + "os": "android" + } + } + }'; + + $response = (new JsonSerializer())->deserialize($json); + $cashApp = $response['payment_methods']['cashapp']; + + $this->assertSame("action_required", $cashApp['status']); + $this->assertSame([], $cashApp['flags']); + $this->assertSame("enabled", $cashApp['initialization']); + $this->assertTrue($cashApp['customer_profile_sharing']); + $this->assertSame("ORDER-99", $cashApp['reference']); + $this->assertSame(CashAppActionType::$redirect, $cashApp['action']['type']); + $this->assertSame(self::CASHAPP_REDIRECT_URL, $cashApp['action']['redirect_url']); + + $profile = $cashApp['customer_profile']; + $this->assertCount(13, $profile); + $this->assertSame(self::CASHAPP_CUSTOMER_ID, $profile['customer_id']); + $this->assertSame('$CASHTAG_C_TOKEN', $profile['cashtag']); + $this->assertSame("value", $profile['reference_id']); + $this->assertSame("John Middle Doe", $profile['full_name']); + $this->assertSame("John", $profile['given_name']); + $this->assertSame("Middle", $profile['middle_name']); + $this->assertSame("Doe", $profile['family_name']); + $this->assertSame("Jr.", $profile['suffix']); + $this->assertSame("1990-01-01T00:00:00.0000000", $profile['birth_date']); + $this->assertSame("5555555555", $profile['phone_number']); + $this->assertSame("cash@cash.com", $profile['email_address']); + $this->assertSame("1970-01-18T12:46:04.8000000+00:00", $profile['customer_since']); + + $address = $profile['address']; + $this->assertCount(8, $address); + $this->assertSame("123 Main St", $address['address_line_1']); + $this->assertSame("Apt 2", $address['address_line_2']); + $this->assertSame("Floor 3", $address['address_line_3']); + $this->assertSame("Springfield", $address['locality']); + $this->assertSame("Downtown", $address['sublocality']); + $this->assertSame("IL", $address['administrative_district_level_1']); + $this->assertSame("62701", $address['postal_code']); + $this->assertSame("US", $address['country']); + + $device = $response['customer']['device']; + $this->assertSame("en_US", $device['locale']); + $this->assertSame("fp_abc123xyz", $device['fingerprint']); + $this->assertSame("203.0.113.0", $device['ipv4']); + $this->assertSame("2001:db8:85a3::8a2e:370:7334", $device['ipv6']); + $this->assertSame(DeviceClient::$web, $device['client']); + $this->assertSame(DeviceOs::$android, $device['os']); + } + + public function testCustomerRoundTripKeepsAllEightProperties() + { + $email = new Email(); + $email->address = "johnsmith@example.com"; + $email->verified = true; + + $phone = new Phone(); + $phone->country_code = "+44"; + $phone->number = "207 946 0000"; + + $device = new Device(); + $device->locale = "en_GB"; + $device->fingerprint = "fp_abc123xyz"; + $device->ipv4 = "203.0.113.0"; + $device->ipv6 = "2001:db8:85a3::8a2e:370:7334"; + $device->client = DeviceClient::$app; + $device->os = DeviceOs::$ios; + + $merchantAccount = new MerchantAccount(); + $merchantAccount->id = "1234"; + $merchantAccount->registration_date = new DateTime("2023-05-01"); + $merchantAccount->last_modified = new DateTime("2023-05-01"); + $merchantAccount->returning_customer = true; + $merchantAccount->first_transaction_date = new DateTime("2023-09-15"); + $merchantAccount->last_transaction_date = new DateTime("2025-03-28"); + $merchantAccount->total_order_count = 6; + $merchantAccount->last_payment_amount = 55; + + $customer = new Customer(); + $customer->email = $email; + $customer->name = "John Smith"; + $customer->phone = $phone; + $customer->device = $device; + $customer->merchant_account = $merchantAccount; + $customer->id = "cus_123456789"; + $customer->country = "GB"; + $customer->tax_number = "GB123456789"; + + $serializer = new JsonSerializer(); + $json = $serializer->serialize($customer); + $decoded = $serializer->deserialize($json); + + $this->assertStringContainsString('"tax_number":"GB123456789"', $json); + $this->assertStringNotContainsString('taxNumber', $json); + $this->assertCount(8, $decoded); + $this->assertSame("cus_123456789", $decoded['id']); + $this->assertSame("GB", $decoded['country']); + $this->assertSame("GB123456789", $decoded['tax_number']); + $this->assertSame("John Smith", $decoded['name']); + $this->assertSame(["address" => "johnsmith@example.com", "verified" => true], $decoded['email']); + $this->assertSame(["country_code" => "+44", "number" => "207 946 0000"], $decoded['phone']); + $this->assertSame( + [ + "locale" => "en_GB", + "fingerprint" => "fp_abc123xyz", + "ipv4" => "203.0.113.0", + "ipv6" => "2001:db8:85a3::8a2e:370:7334", + "client" => "app", + "os" => "ios", + ], + $decoded['device'] + ); + $this->assertSame( + [ + "id" => "1234", + "registration_date" => "2023-05-01", + "last_modified" => "2023-05-01", + "returning_customer" => true, + "first_transaction_date" => "2023-09-15", + "last_transaction_date" => "2025-03-28", + "total_order_count" => 6, + "last_payment_amount" => 55, + ], + $decoded['merchant_account'] + ); + } + + public function testDeserializesCustomerSwaggerExample() + { + $json = '{ + "customer": { + "country": "GB", + "id": "cus_123456789", + "email": { "address": "johnsmith@example.com", "verified": true }, + "name": "John Smith", + "tax_number": "GB123456789", + "phone": { "country_code": "+44", "number": "207 946 0000" }, + "device": { "locale": "en_GB" } + } + }'; + + $customer = (new JsonSerializer())->deserialize($json)['customer']; + + $this->assertSame("GB", $customer['country']); + $this->assertSame("cus_123456789", $customer['id']); + $this->assertSame("johnsmith@example.com", $customer['email']['address']); + $this->assertTrue($customer['email']['verified']); + $this->assertSame("John Smith", $customer['name']); + $this->assertSame("GB123456789", $customer['tax_number']); + $this->assertSame("+44", $customer['phone']['country_code']); + $this->assertSame("207 946 0000", $customer['phone']['number']); + $this->assertSame("en_GB", $customer['device']['locale']); + } } diff --git a/test/Checkout/Tests/Payments/Setups/PaymentSetupsClientTest.php b/test/Checkout/Tests/Payments/Setups/PaymentSetupsClientTest.php index b6309278..0eafd9f1 100644 --- a/test/Checkout/Tests/Payments/Setups/PaymentSetupsClientTest.php +++ b/test/Checkout/Tests/Payments/Setups/PaymentSetupsClientTest.php @@ -115,6 +115,28 @@ public function shouldConfirmPaymentSetup() $this->assertNotNull($response); } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldConfirmPaymentSetupWithCashApp() + { + $paymentSetupId = "setup_123456"; + + $this->apiClient + ->expects($this->once()) + ->method("post") + ->with( + $this->equalTo("payments/setups/" . $paymentSetupId . "/confirm/cashapp"), + $this->isNull(), + $this->anything() + ) + ->willReturn(["response"]); + + $response = $this->client->confirmPaymentSetup($paymentSetupId, "cashapp"); + $this->assertNotNull($response); + } + /** * @test * @throws CheckoutApiException diff --git a/test/Checkout/Tests/Payments/Setups/PaymentSetupsIntegrationTest.php b/test/Checkout/Tests/Payments/Setups/PaymentSetupsIntegrationTest.php index c61dde21..8e67ddb8 100644 --- a/test/Checkout/Tests/Payments/Setups/PaymentSetupsIntegrationTest.php +++ b/test/Checkout/Tests/Payments/Setups/PaymentSetupsIntegrationTest.php @@ -16,6 +16,9 @@ use Checkout\Payments\Setups\Common\Customer\Customer; use Checkout\Payments\Setups\Common\Customer\Email; use Checkout\Payments\Setups\Common\Customer\Device; +use Checkout\Payments\Setups\Common\Customer\DeviceClient; +use Checkout\Payments\Setups\Common\Customer\DeviceOs; +use Checkout\Payments\Setups\Common\PaymentMethods\CashApp\CashApp; use Checkout\Payments\PaymentType; use Checkout\PlatformType; use Checkout\Tests\SandboxTestFixture; @@ -137,6 +140,83 @@ public function shouldConfirmPaymentSetup() $this->assertNotNull($response["processed_on"]); } + /** + * @test + * @throws CheckoutApiException + */ + public function shouldCreatePaymentSetupWithDeviceFieldsAndEchoThem() + { + // Arrange + $request = $this->createPaymentSetupRequest(); + $request->customer->device->fingerprint = "fp_abc123xyz"; + $request->customer->device->ipv4 = "203.0.113.0"; + $request->customer->device->ipv6 = "2001:db8:85a3::8a2e:370:7334"; + $request->customer->device->client = DeviceClient::$web; + $request->customer->device->os = DeviceOs::$android; + + // Act + $response = $this->checkoutApi->getPaymentSetupsClient()->createPaymentSetup($request); + + // Assert + $device = $response["customer"]["device"]; + $this->assertEquals("en_GB", $device["locale"]); + $this->assertEquals("fp_abc123xyz", $device["fingerprint"]); + $this->assertEquals("203.0.113.0", $device["ipv4"]); + $this->assertEquals("2001:db8:85a3::8a2e:370:7334", $device["ipv6"]); + $this->assertEquals("web", $device["client"]); + $this->assertEquals("android", $device["os"]); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldCreatePaymentSetupWithCustomerIdentifiersAndEchoThem() + { + // Arrange + $request = $this->createPaymentSetupRequest(); + $request->customer->id = "cus_123456789"; + $request->customer->country = "GB"; + $request->customer->tax_number = "GB123456789"; + + // Act + $response = $this->checkoutApi->getPaymentSetupsClient()->createPaymentSetup($request); + + // Assert + $this->assertEquals("cus_123456789", $response["customer"]["id"]); + $this->assertEquals("GB", $response["customer"]["country"]); + $this->assertEquals("GB123456789", $response["customer"]["tax_number"]); + } + + /** + * @test + * @throws CheckoutApiException + */ + public function shouldCreatePaymentSetupWithCashAppAndReturnItsState() + { + // Arrange + $request = $this->createPaymentSetupRequest(); + $request->currency = Currency::$USD; + $request->customer->device->client = DeviceClient::$web; + $cashApp = new CashApp(); + $cashApp->initialization = "enabled"; + $cashApp->customer_profile_sharing = true; + $request->payment_methods = new PaymentMethods(); + $request->payment_methods->cashapp = $cashApp; + + // Act + $response = $this->checkoutApi->getPaymentSetupsClient()->createPaymentSetup($request); + + // Assert + $available = isset($response["available_payment_methods"]) ? $response["available_payment_methods"] : []; + if (!in_array("cashapp", $available, true)) { + $this->markTestSkipped("Cash App Pay is not enabled on the sandbox processing channel"); + } + $this->assertArrayHasKey("cashapp", $response["payment_methods"]); + $this->assertNotNull($response["payment_methods"]["cashapp"]["status"]); + $this->assertEquals("enabled", $response["payment_methods"]["cashapp"]["initialization"]); + } + /** * @return PaymentSetupRequest */ From 3f4322844f09a94dace7ae9c8f5d1b7eb8647e28 Mon Sep 17 00:00:00 2001 From: david ruiz Date: Wed, 7 Oct 2026 16:33:39 +0200 Subject: [PATCH 2/3] More docs and tests --- .../Payments/Setups/Common/Customer/Customer.php | 3 +++ .../Payments/Setups/Common/Customer/Device.php | 3 +++ .../Setups/Common/Customer/DeviceClient.php | 14 ++++++++++++++ .../Payments/Setups/Common/Customer/DeviceOs.php | 9 +++++++++ .../Common/PaymentMethods/CashApp/CashApp.php | 4 +++- .../PaymentMethods/CashApp/CashAppActionType.php | 4 ++++ .../CashApp/CashAppCustomerProfile.php | 11 +++++------ .../Setups/PaymentSetupFieldsSerializationTest.php | 14 ++++++++++++++ .../Setups/PaymentSetupsIntegrationTest.php | 9 ++++++--- 9 files changed, 61 insertions(+), 10 deletions(-) diff --git a/lib/Checkout/Payments/Setups/Common/Customer/Customer.php b/lib/Checkout/Payments/Setups/Common/Customer/Customer.php index 84f43ea7..d18f04c5 100644 --- a/lib/Checkout/Payments/Setups/Common/Customer/Customer.php +++ b/lib/Checkout/Payments/Setups/Common/Customer/Customer.php @@ -4,6 +4,9 @@ use Checkout\Common\Phone; +/** + * The customer's details. + */ class Customer { /** diff --git a/lib/Checkout/Payments/Setups/Common/Customer/Device.php b/lib/Checkout/Payments/Setups/Common/Customer/Device.php index 2953dda4..24a1cc3b 100644 --- a/lib/Checkout/Payments/Setups/Common/Customer/Device.php +++ b/lib/Checkout/Payments/Setups/Common/Customer/Device.php @@ -2,6 +2,9 @@ namespace Checkout\Payments\Setups\Common\Customer; +/** + * Details of the customer's device. + */ class Device { /** diff --git a/lib/Checkout/Payments/Setups/Common/Customer/DeviceClient.php b/lib/Checkout/Payments/Setups/Common/Customer/DeviceClient.php index 76c5cede..6ad9d20f 100644 --- a/lib/Checkout/Payments/Setups/Common/Customer/DeviceClient.php +++ b/lib/Checkout/Payments/Setups/Common/Customer/DeviceClient.php @@ -7,7 +7,21 @@ */ class DeviceClient { + /** + * A web browser on a desktop device. + * @var string + */ public static $web = "web"; + + /** + * A web browser on a mobile device. + * @var string + */ public static $mobile_web = "mobile_web"; + + /** + * A native mobile application. + * @var string + */ public static $app = "app"; } diff --git a/lib/Checkout/Payments/Setups/Common/Customer/DeviceOs.php b/lib/Checkout/Payments/Setups/Common/Customer/DeviceOs.php index 93151083..fedacad5 100644 --- a/lib/Checkout/Payments/Setups/Common/Customer/DeviceOs.php +++ b/lib/Checkout/Payments/Setups/Common/Customer/DeviceOs.php @@ -7,6 +7,15 @@ */ class DeviceOs { + /** + * Android. + * @var string + */ public static $android = "android"; + + /** + * iOS. + * @var string + */ public static $ios = "ios"; } diff --git a/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashApp.php b/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashApp.php index 6da25181..5a8ed84c 100644 --- a/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashApp.php +++ b/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashApp.php @@ -8,7 +8,9 @@ * The Cash App payment method's details and configuration. * * Inherits status, flags and initialization from PaymentMethodBase. Send initialization and - * customer_profile_sharing; the remaining properties are returned by the API only. + * customer_profile_sharing; the remaining properties are returned by the API only. To authorize the + * payment, send the customer to the redirect URL returned in the action. The customer device client + * (Device::$client) is required when using this payment method. */ class CashApp extends PaymentMethodBase { diff --git a/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashAppActionType.php b/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashAppActionType.php index 4ea1aa7e..08235357 100644 --- a/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashAppActionType.php +++ b/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashAppActionType.php @@ -7,5 +7,9 @@ */ class CashAppActionType { + /** + * Redirect the customer to Cash App to authorize the payment. + * @var string + */ public static $redirect = "redirect"; } diff --git a/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashAppCustomerProfile.php b/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashAppCustomerProfile.php index ab602114..f69aba85 100644 --- a/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashAppCustomerProfile.php +++ b/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashAppCustomerProfile.php @@ -72,12 +72,11 @@ class CashAppCustomerProfile public $suffix; /** - * The customer's date of birth. + * The customer's date of birth. Kept as a string because the provider's format varies (the API + * example is a date-time, for example 1990-01-01T00:00:00.0000000). * [Optional] * readOnly * Format: date - * Kept as a string because the provider's format varies (the API example is a date-time, - * for example 1990-01-01T00:00:00.0000000). * @var string */ public $birth_date; @@ -107,12 +106,12 @@ class CashAppCustomerProfile public $email_address; /** - * The date and time the customer's Cash App account was created. + * The date and time the customer's Cash App account was created. Kept as a string because the + * provider's format varies (for example 1970-01-18T12:46:04.8000000+00:00, with seven + * fractional digits). * [Optional] * readOnly * Format: date-time - * Kept as a string because the provider's format varies (for example - * 1970-01-18T12:46:04.8000000+00:00, with seven fractional digits). * @var string */ public $customer_since; diff --git a/test/Checkout/Tests/Payments/Setups/PaymentSetupFieldsSerializationTest.php b/test/Checkout/Tests/Payments/Setups/PaymentSetupFieldsSerializationTest.php index fee6781a..d12389a5 100644 --- a/test/Checkout/Tests/Payments/Setups/PaymentSetupFieldsSerializationTest.php +++ b/test/Checkout/Tests/Payments/Setups/PaymentSetupFieldsSerializationTest.php @@ -323,6 +323,19 @@ public function testSerializesCashAppUnderLiteralKeyWithMerchantFields() ); } + public function testSerializesCustomerProfileSharingFalse() + { + $cashApp = new CashApp(); + $cashApp->initialization = "enabled"; + $cashApp->customer_profile_sharing = false; + + $decoded = json_decode((new JsonSerializer())->serialize($cashApp), true); + + // The serializer strips nulls only; an explicit false must still reach the API. + $this->assertArrayHasKey("customer_profile_sharing", $decoded); + $this->assertFalse($decoded["customer_profile_sharing"]); + } + public function testSerializesEveryDeviceClientValue() { $serializer = new JsonSerializer(); @@ -437,6 +450,7 @@ public function testCashAppRoundTripKeepsEveryProperty() $this->assertStringContainsString('"address_line_3":', $json); $this->assertStringContainsString('"administrative_district_level_1":', $json); $this->assertStringNotContainsString('"address_line1"', $json); + $this->assertStringNotContainsString('"administrative_district_level1"', $json); $this->assertSame("action_required", $decoded['status']); $this->assertSame([], $decoded['flags']); diff --git a/test/Checkout/Tests/Payments/Setups/PaymentSetupsIntegrationTest.php b/test/Checkout/Tests/Payments/Setups/PaymentSetupsIntegrationTest.php index 8e67ddb8..f6294b4f 100644 --- a/test/Checkout/Tests/Payments/Setups/PaymentSetupsIntegrationTest.php +++ b/test/Checkout/Tests/Payments/Setups/PaymentSetupsIntegrationTest.php @@ -212,9 +212,12 @@ public function shouldCreatePaymentSetupWithCashAppAndReturnItsState() if (!in_array("cashapp", $available, true)) { $this->markTestSkipped("Cash App Pay is not enabled on the sandbox processing channel"); } - $this->assertArrayHasKey("cashapp", $response["payment_methods"]); - $this->assertNotNull($response["payment_methods"]["cashapp"]["status"]); - $this->assertEquals("enabled", $response["payment_methods"]["cashapp"]["initialization"]); + $fetched = $this->checkoutApi->getPaymentSetupsClient()->getPaymentSetup($response["id"]); + $this->assertArrayHasKey("cashapp", $fetched["payment_methods"]); + $cashAppResponse = $fetched["payment_methods"]["cashapp"]; + $this->assertNotNull($cashAppResponse["status"]); + $this->assertEquals("enabled", $cashAppResponse["initialization"]); + $this->assertTrue($cashAppResponse["customer_profile_sharing"]); } /** From 35f8ccf58890f7e24063fd5b446a09337a4f926b Mon Sep 17 00:00:00 2001 From: david ruiz Date: Thu, 8 Oct 2026 15:31:31 +0200 Subject: [PATCH 3/3] String values enum added + tests --- .../Common/PaymentMethods/CashApp/CashApp.php | 2 +- .../Common/PaymentMethodBase.php | 4 +- .../Common/PaymentMethodInitialization.php | 15 +++++++ .../Common/PaymentMethodStatus.php | 24 ++++++++++ .../PaymentSetupFieldsSerializationTest.php | 45 ++++++++++++++++++- 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 lib/Checkout/Payments/Setups/Common/PaymentMethods/Common/PaymentMethodInitialization.php create mode 100644 lib/Checkout/Payments/Setups/Common/PaymentMethods/Common/PaymentMethodStatus.php diff --git a/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashApp.php b/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashApp.php index 5a8ed84c..718d8fcc 100644 --- a/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashApp.php +++ b/lib/Checkout/Payments/Setups/Common/PaymentMethods/CashApp/CashApp.php @@ -10,7 +10,7 @@ * Inherits status, flags and initialization from PaymentMethodBase. Send initialization and * customer_profile_sharing; the remaining properties are returned by the API only. To authorize the * payment, send the customer to the redirect URL returned in the action. The customer device client - * (Device::$client) is required when using this payment method. + * (customer.device.client) is required when using this payment method. */ class CashApp extends PaymentMethodBase { diff --git a/lib/Checkout/Payments/Setups/Common/PaymentMethods/Common/PaymentMethodBase.php b/lib/Checkout/Payments/Setups/Common/PaymentMethods/Common/PaymentMethodBase.php index b22e6be3..3d13a8b5 100644 --- a/lib/Checkout/Payments/Setups/Common/PaymentMethods/Common/PaymentMethodBase.php +++ b/lib/Checkout/Payments/Setups/Common/PaymentMethods/Common/PaymentMethodBase.php @@ -9,7 +9,7 @@ abstract class PaymentMethodBase * [Optional] * readOnly * Enum: "unavailable" "action_required" "ready" "initialization_required" "invalid" - * @var string + * @var string value of PaymentMethodStatus */ public $status; @@ -27,7 +27,7 @@ abstract class PaymentMethodBase * [Optional] * Default: "disabled" * Enum: "disabled" "enabled" - * @var string + * @var string value of PaymentMethodInitialization */ public $initialization; } diff --git a/lib/Checkout/Payments/Setups/Common/PaymentMethods/Common/PaymentMethodInitialization.php b/lib/Checkout/Payments/Setups/Common/PaymentMethods/Common/PaymentMethodInitialization.php new file mode 100644 index 00000000..f87e0aed --- /dev/null +++ b/lib/Checkout/Payments/Setups/Common/PaymentMethods/Common/PaymentMethodInitialization.php @@ -0,0 +1,15 @@ +assertSame("+44", $customer['phone']['country_code']); $this->assertSame("207 946 0000", $customer['phone']['number']); $this->assertSame("en_GB", $customer['device']['locale']); + $this->assertSame("fp_abc123xyz", $customer['device']['fingerprint']); + $this->assertSame("203.0.113.0", $customer['device']['ipv4']); + $this->assertSame("2001:db8:85a3::8a2e:370:7334", $customer['device']['ipv6']); + $this->assertSame(DeviceClient::$web, $customer['device']['client']); + $this->assertSame(DeviceOs::$android, $customer['device']['os']); + $this->assertSame("1234", $customer['merchant_account']['id']); + $this->assertSame("2023-05-01T00:00:00.0000000", $customer['merchant_account']['registration_date']); + $this->assertSame("2023-05-01T00:00:00.0000000", $customer['merchant_account']['last_modified']); + $this->assertTrue($customer['merchant_account']['returning_customer']); + $this->assertSame("2023-09-15T00:00:00.0000000", $customer['merchant_account']['first_transaction_date']); + $this->assertSame("2025-03-28T00:00:00.0000000", $customer['merchant_account']['last_transaction_date']); + $this->assertSame(6, $customer['merchant_account']['total_order_count']); + $this->assertSame(55.99, $customer['merchant_account']['last_payment_amount']); + } + + public function testPaymentMethodStatusAndInitializationConstantsMatchTheSpec() + { + $this->assertSame("unavailable", PaymentMethodStatus::$unavailable); + $this->assertSame("action_required", PaymentMethodStatus::$action_required); + $this->assertSame("ready", PaymentMethodStatus::$ready); + $this->assertSame("initialization_required", PaymentMethodStatus::$initialization_required); + $this->assertSame("invalid", PaymentMethodStatus::$invalid); + $this->assertSame("disabled", PaymentMethodInitialization::$disabled); + $this->assertSame("enabled", PaymentMethodInitialization::$enabled); } }