diff --git a/CHANGELOG.md b/CHANGELOG.md index 2076688..9d07afc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), Check our main [developer changelog](https://developer.paddle.com/?utm_source=dx&utm_medium=paddle-php-sdk) for information about changes to the Paddle Billing platform, the Paddle API, and other developer tools. +## [Unreleased] + +### Added + +- Support for saved payment methods, see [related changelog](https://developer.paddle.com/changelog/2024/saved-payment-methods?utm_source=dx&utm_medium=paddle-php-sdk) + - `Client->paymentMethods->list()` + - `Client->paymentMethods->get()` + - `Client->paymentMethods->delete()` + - `Client->customers->generateAuthToken()` + ## [1.4.0] - 2024-10-17 ### Added diff --git a/examples/customer_auth_token.php b/examples/customer_auth_token.php new file mode 100644 index 0000000..004f365 --- /dev/null +++ b/examples/customer_auth_token.php @@ -0,0 +1,33 @@ +customers->generateAuthToken($customerId); +} catch (ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo sprintf("Created Customer Auth Token: %s\n", $authToken->customerAuthToken); +echo sprintf(" - Expires At: %s\n", $authToken->expiresAt->format(DATE_RFC3339_EXTENDED)); diff --git a/examples/payment_methods.php b/examples/payment_methods.php new file mode 100644 index 0000000..e1d3e0e --- /dev/null +++ b/examples/payment_methods.php @@ -0,0 +1,97 @@ +paymentMethods->list( + $customerId, + new ListPaymentMethods( + pager: new Pager(perPage: 10), + ), + ); +} catch (ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo "List Payment Methods\n"; + +foreach ($paymentMethods as $paymentMethod) { + echo sprintf("- %s:\n", $paymentMethod->id); + echo sprintf(" - Type: %s\n", $paymentMethod->type->getValue()); + + if ($paymentMethod->card) { + echo sprintf(" - Card Type: %s\n", $paymentMethod->card->type->getValue()); + echo sprintf(" - Card Holder Name: %s\n", $paymentMethod->card->cardholderName); + echo sprintf(" - Card Last 4 Digits: %s\n", $paymentMethod->card->last4); + echo sprintf(" - Card Expiry Year: %d\n", $paymentMethod->card->expiryYear); + echo sprintf(" - Card Expiry Month: %d\n", $paymentMethod->card->expiryMonth); + } + + if ($paymentMethod->paypal) { + echo sprintf(" - PayPal Reference: %s\n", $paymentMethod->paypal->reference); + echo sprintf(" - PayPal Email: %s\n", $paymentMethod->paypal->email); + } +} + +// ┌─── +// │ Get Payment Method | +// └────────────────────┘ +try { + $paymentMethod = $paddle->paymentMethods->get($customerId, $paymentMethodId); +} catch (ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo sprintf("Get Payment Method: %s\n", $paymentMethod->id); +echo sprintf(" - Type: %s\n", $paymentMethod->type->getValue()); + +if ($paymentMethod->card) { + echo sprintf(" - Card Type: %s\n", $paymentMethod->card->type->getValue()); + echo sprintf(" - Card Holder Name: %s\n", $paymentMethod->card->cardholderName); + echo sprintf(" - Card Last 4 Digits: %s\n", $paymentMethod->card->last4); + echo sprintf(" - Card Expiry Year: %d\n", $paymentMethod->card->expiryYear); + echo sprintf(" - Card Expiry Month: %d\n", $paymentMethod->card->expiryMonth); +} + +if ($paymentMethod->paypal) { + echo sprintf(" - PayPal Reference: %s\n", $paymentMethod->paypal->reference); + echo sprintf(" - PayPal Email: %s\n", $paymentMethod->paypal->email); +} + +// ┌─── +// │ Delete Payment Method | +// └───────────────────────┘ +try { + $paddle->paymentMethods->delete($customerId, $paymentMethodId); +} catch (ApiError|MalformedResponse $e) { + var_dump($e); + exit; +} + +echo sprintf("Deleted Payment Method: %s\n", $paymentMethodId); +echo sprintf(" - Type: %s\n", $paymentMethod->type->getValue()); diff --git a/src/Client.php b/src/Client.php index 0c6728c..9bc5df7 100644 --- a/src/Client.php +++ b/src/Client.php @@ -29,6 +29,7 @@ use Paddle\SDK\Resources\NotificationLogs\NotificationLogsClient; use Paddle\SDK\Resources\Notifications\NotificationsClient; use Paddle\SDK\Resources\NotificationSettings\NotificationSettingsClient; +use Paddle\SDK\Resources\PaymentMethods\PaymentMethodsClient; use Paddle\SDK\Resources\Prices\PricesClient; use Paddle\SDK\Resources\PricingPreviews\PricingPreviewsClient; use Paddle\SDK\Resources\Products\ProductsClient; @@ -75,6 +76,7 @@ class Client public readonly EventTypesClient $eventTypes; public readonly EventsClient $events; public readonly PricingPreviewsClient $pricingPreviews; + public readonly PaymentMethodsClient $paymentMethods; public readonly NotificationSettingsClient $notificationSettings; public readonly NotificationsClient $notifications; public readonly NotificationLogsClient $notificationLogs; @@ -122,6 +124,7 @@ public function __construct( $this->eventTypes = new EventTypesClient($this); $this->events = new EventsClient($this); $this->pricingPreviews = new PricingPreviewsClient($this); + $this->paymentMethods = new PaymentMethodsClient($this); $this->notificationSettings = new NotificationSettingsClient($this); $this->notifications = new NotificationsClient($this); $this->notificationLogs = new NotificationLogsClient($this); @@ -153,7 +156,7 @@ public function patchRaw(string|UriInterface $uri, array|\JsonSerializable $payl return $this->requestRaw('PATCH', $uri, $payload); } - public function postRaw(string|UriInterface $uri, array|\JsonSerializable $payload = [], array|HasParameters $parameters = []): ResponseInterface + public function postRaw(string|UriInterface $uri, array|\JsonSerializable|null $payload = [], array|HasParameters $parameters = []): ResponseInterface { if ($parameters) { $parameters = $parameters instanceof HasParameters ? $parameters->getParameters() : $parameters; diff --git a/src/Entities/Collections/PaymentMethodCollection.php b/src/Entities/Collections/PaymentMethodCollection.php new file mode 100644 index 0000000..544bd30 --- /dev/null +++ b/src/Entities/Collections/PaymentMethodCollection.php @@ -0,0 +1,30 @@ + PaymentMethod::from($item), $itemsData), + $paginator, + ); + } + + public function current(): PaymentMethod + { + return parent::current(); + } +} diff --git a/src/Entities/CustomerAuthToken.php b/src/Entities/CustomerAuthToken.php new file mode 100644 index 0000000..3ddfaa2 --- /dev/null +++ b/src/Entities/CustomerAuthToken.php @@ -0,0 +1,32 @@ + DeletedPaymentMethod::class, + ]; + + $entity = $eventEntityTypes[$eventType] ?? self::resolveEntityClass($eventType); + + return $entity::from($data); + } + + /** + * @return class-string + */ + private static function resolveEntityClass(string $eventType): string { $type = explode('.', $eventType); - $entity = $type[0] ?? 'Unknown'; - $identifier = str_replace('_', '', ucwords(implode('_', $type), '_')); + $entity = self::snakeToPascalCase($type[0] ?? 'Unknown'); + $identifier = self::snakeToPascalCase(implode('_', $type)); /** @var class-string $entity */ - $entity = sprintf('\Paddle\SDK\Notifications\Entities\%s', ucfirst($entity)); + $entity = sprintf('\Paddle\SDK\Notifications\Entities\%s', $entity); if (! class_exists($entity)) { $entity = UndefinedEntity::class; } @@ -22,6 +37,11 @@ public static function create(string $eventType, array $data): Entity throw new \UnexpectedValueException("Event type '{$identifier}' cannot be mapped to an object"); } - return $entity::from($data); + return $entity; + } + + private static function snakeToPascalCase(string $string): string + { + return str_replace('_', '', ucwords($string, '_')); } } diff --git a/src/Notifications/Entities/PaymentMethod.php b/src/Notifications/Entities/PaymentMethod.php new file mode 100644 index 0000000..1930ba5 --- /dev/null +++ b/src/Notifications/Entities/PaymentMethod.php @@ -0,0 +1,43 @@ +getData()); } + + /** + * @throws ApiError On a generic API error + * @throws ApiError\CustomerApiError On a customer specific API error + * @throws MalformedResponse If the API response was not parsable + */ + public function generateAuthToken(string $id): CustomerAuthToken + { + $parser = new ResponseParser( + $this->client->postRaw("/customers/{$id}/auth-token", null), + ); + + return CustomerAuthToken::from($parser->getData()); + } } diff --git a/src/Resources/PaymentMethods/Operations/ListPaymentMethods.php b/src/Resources/PaymentMethods/Operations/ListPaymentMethods.php new file mode 100644 index 0000000..ee66204 --- /dev/null +++ b/src/Resources/PaymentMethods/Operations/ListPaymentMethods.php @@ -0,0 +1,38 @@ + $addressIds + * + * @throws InvalidArgumentException If addressIds contain the incorrect type + */ + public function __construct( + private readonly Pager|null $pager = null, + private readonly array $addressIds = [], + private readonly bool|null $supportsCheckout = null, + ) { + if ($invalid = array_filter($this->addressIds, fn ($value): bool => ! is_string($value))) { + throw InvalidArgumentException::arrayContainsInvalidTypes('address_ids', 'string', implode(', ', $invalid)); + } + } + + public function getParameters(): array + { + return array_merge( + $this->pager?->getParameters() ?? [], + array_filter([ + 'address_id' => implode(',', $this->addressIds), + 'supports_checkout' => isset($this->supportsCheckout) ? ($this->supportsCheckout ? 'true' : 'false') : null, + ]), + ); + } +} diff --git a/src/Resources/PaymentMethods/PaymentMethodsClient.php b/src/Resources/PaymentMethods/PaymentMethodsClient.php new file mode 100644 index 0000000..9d732a3 --- /dev/null +++ b/src/Resources/PaymentMethods/PaymentMethodsClient.php @@ -0,0 +1,69 @@ +client->getRaw("/customers/{$customerId}/payment-methods", $listOperation), + ); + + return PaymentMethodCollection::from( + $parser->getData(), + new Paginator($this->client, $parser->getPagination(), PaymentMethodCollection::class), + ); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function get(string $customerId, string $id): PaymentMethod + { + $parser = new ResponseParser( + $this->client->getRaw("/customers/{$customerId}/payment-methods/{$id}"), + ); + + return PaymentMethod::from($parser->getData()); + } + + /** + * @throws ApiError On a generic API error + * @throws MalformedResponse If the API response was not parsable + */ + public function delete(string $customerId, string $id): void + { + new ResponseParser( + $this->client->deleteRaw("/customers/{$customerId}/payment-methods/{$id}"), + ); + } +} diff --git a/tests/Functional/Resources/Customers/CustomersClientTest.php b/tests/Functional/Resources/Customers/CustomersClientTest.php index b5c66f8..e70e092 100644 --- a/tests/Functional/Resources/Customers/CustomersClientTest.php +++ b/tests/Functional/Resources/Customers/CustomersClientTest.php @@ -243,4 +243,31 @@ public static function creditBalancesOperationsProvider(): \Generator sprintf('%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/credit-balances', Environment::SANDBOX->baseUrl()), ]; } + + /** + * @test + */ + public function create_auth_token_hits_expected_uri_and_parses_response(): void + { + $expectedUri = sprintf('%s/customers/ctm_01h8441jn5pcwrfhwh78jqt8hk/auth-token', Environment::SANDBOX->baseUrl()); + $response = new Response(200, body: self::readRawJsonFixture('response/auth_token')); + + $this->mockClient->addResponse($response); + $authToken = $this->client->customers->generateAuthToken('ctm_01h8441jn5pcwrfhwh78jqt8hk'); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertSame('', (string) $request->getBody()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + + self::assertSame( + 'pca_01hwyzq8hmdwed5p4jc4hnv6bh_01hwwggymjn0yhhb2gr4p91276_6xaav4lydudt6bgmuefeaf2xnu3umegx', + $authToken->customerAuthToken, + ); + self::assertSame( + '2024-05-03T10:34:12.345+00:00', + $authToken->expiresAt->format(DATE_RFC3339_EXTENDED), + ); + } } diff --git a/tests/Functional/Resources/Customers/_fixtures/response/auth_token.json b/tests/Functional/Resources/Customers/_fixtures/response/auth_token.json new file mode 100644 index 0000000..d1cc6d5 --- /dev/null +++ b/tests/Functional/Resources/Customers/_fixtures/response/auth_token.json @@ -0,0 +1,9 @@ +{ + "data": { + "customer_auth_token": "pca_01hwyzq8hmdwed5p4jc4hnv6bh_01hwwggymjn0yhhb2gr4p91276_6xaav4lydudt6bgmuefeaf2xnu3umegx", + "expires_at": "2024-05-03T10:34:12.345Z" + }, + "meta": { + "request_id": "fa176777-4bca-49ec-aa1e-f53885333cb7" + } +} diff --git a/tests/Functional/Resources/PaymentMethods/PaymentMethodsClientTest.php b/tests/Functional/Resources/PaymentMethods/PaymentMethodsClientTest.php new file mode 100644 index 0000000..c14821f --- /dev/null +++ b/tests/Functional/Resources/PaymentMethods/PaymentMethodsClientTest.php @@ -0,0 +1,254 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider listOperationsProvider + */ + public function list_hits_expected_uri( + string $customerId, + ListPaymentMethods $listOperation, + string $expectedUri, + ): void { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_default'))); + $this->client->paymentMethods->list($customerId, $listOperation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } + + public static function listOperationsProvider(): \Generator + { + yield 'Default' => [ + 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4', + new ListPaymentMethods(), + sprintf('%s/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods', Environment::SANDBOX->baseUrl()), + ]; + + yield 'List by address IDs' => [ + 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4', + new ListPaymentMethods( + addressIds: ['add_01hv8h6jj90jjz0d71m6hj4r9z', 'add_02hv8h6jj90jjz0d71m6hj4r9z'], + ), + sprintf( + '%s/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?address_id=add_01hv8h6jj90jjz0d71m6hj4r9z,add_02hv8h6jj90jjz0d71m6hj4r9z', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'List supports_checkout false' => [ + 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4', + new ListPaymentMethods( + supportsCheckout: false, + ), + sprintf('%s/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?supports_checkout=false', Environment::SANDBOX->baseUrl()), + ]; + + yield 'List supports_checkout true' => [ + 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4', + new ListPaymentMethods( + supportsCheckout: true, + ), + sprintf('%s/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?supports_checkout=true', Environment::SANDBOX->baseUrl()), + ]; + + yield 'List by address IDs and supports_checkout' => [ + 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4', + new ListPaymentMethods( + addressIds: ['add_01hv8h6jj90jjz0d71m6hj4r9z', 'add_02hv8h6jj90jjz0d71m6hj4r9z'], + supportsCheckout: true, + ), + sprintf( + '%s/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?address_id=add_01hv8h6jj90jjz0d71m6hj4r9z,add_02hv8h6jj90jjz0d71m6hj4r9z&supports_checkout=true', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'List payment-methods with pagination' => [ + 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4', + new ListPaymentMethods( + pager: new Pager(), + ), + sprintf( + '%s/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?order_by=id[asc]&per_page=50', + Environment::SANDBOX->baseUrl(), + ), + ]; + + yield 'List payment-methods with pagination after' => [ + 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4', + new ListPaymentMethods( + pager: new Pager( + after: 'paymtd_01hs8zx6x377xfsfrt2bqsevbw', + orderBy: OrderBy::idDescending(), + perPage: 100, + ), + ), + sprintf( + '%s/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?after=paymtd_01hs8zx6x377xfsfrt2bqsevbw&order_by=id[desc]&per_page=100', + Environment::SANDBOX->baseUrl(), + ), + ]; + } + + /** @test */ + public function it_can_paginate(): void + { + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_paginated_page_one'))); + $this->mockClient->addResponse(new Response(200, body: self::readRawJsonFixture('response/list_paginated_page_two'))); + + $collection = $this->client->paymentMethods->list('ctm_01hv6y1jedq4p1n0yqn5ba3ky4'); + + $request = $this->mockClient->getLastRequest(); + + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods', + urldecode((string) $request->getUri()), + ); + + $allPaymentMethods = iterator_to_array($collection); + self::assertCount(4, $allPaymentMethods); + + $request = $this->mockClient->getLastRequest(); + + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?after=paymtd_02hs8zx6x377xfsfrt2bqsevbw', + urldecode((string) $request->getUri()), + ); + } + + /** + * @test + */ + public function get_payment_methods_returns_expected_card_response(): void + { + $customerId = 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4'; + $paymentMethodId = 'paymtd_01hs8zx6x377xfsfrt2bqsevbw'; + $expectedUri = sprintf( + '%s/customers/%s/payment-methods/%s', + Environment::SANDBOX->baseUrl(), + $customerId, + $paymentMethodId, + ); + $response = new Response(200, body: self::readRawJsonFixture('response/full_entity_card')); + + $this->mockClient->addResponse($response); + $paymentMethod = $this->client->paymentMethods->get($customerId, $paymentMethodId); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + + self::assertSame($paymentMethodId, $paymentMethod->id); + self::assertSame($customerId, $paymentMethod->customerId); + self::assertSame('add_01hv8h6jj90jjz0d71m6hj4r9z', $paymentMethod->addressId); + self::assertNull($paymentMethod->paypal); + self::assertEquals(SavedPaymentMethodOrigin::Subscription(), $paymentMethod->origin); + self::assertSame('2024-05-03T11:50:23.422+00:00', $paymentMethod->savedAt->format(DATE_RFC3339_EXTENDED)); + self::assertSame('2024-05-04T11:50:23.422+00:00', $paymentMethod->updatedAt->format(DATE_RFC3339_EXTENDED)); + + $card = $paymentMethod->card; + self::assertEquals('visa', $card->type); + self::assertEquals('0002', $card->last4); + self::assertEquals(1, $card->expiryMonth); + self::assertEquals(2025, $card->expiryYear); + self::assertEquals('Sam Miller', $card->cardholderName); + } + + /** + * @test + */ + public function get_payment_methods_returns_expected_paypal_response(): void + { + $customerId = 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4'; + $paymentMethodId = 'paymtd_01hs8zx6x377xfsfrt2bqsevbw'; + $expectedUri = sprintf( + '%s/customers/%s/payment-methods/%s', + Environment::SANDBOX->baseUrl(), + $customerId, + $paymentMethodId, + ); + $response = new Response(200, body: self::readRawJsonFixture('response/full_entity_paypal')); + + $this->mockClient->addResponse($response); + $paymentMethod = $this->client->paymentMethods->get($customerId, $paymentMethodId); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('GET', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + + self::assertSame($paymentMethodId, $paymentMethod->id); + self::assertSame($customerId, $paymentMethod->customerId); + self::assertSame('add_01hv8h6jj90jjz0d71m6hj4r9z', $paymentMethod->addressId); + self::assertNull($paymentMethod->card); + self::assertEquals(SavedPaymentMethodOrigin::SavedDuringPurchase(), $paymentMethod->origin); + self::assertSame('2024-05-03T11:50:23.422+00:00', $paymentMethod->savedAt->format(DATE_RFC3339_EXTENDED)); + self::assertSame('2024-05-04T11:50:23.422+00:00', $paymentMethod->updatedAt->format(DATE_RFC3339_EXTENDED)); + + $paypal = $paymentMethod->paypal; + self::assertEquals('sam@example.com', $paypal->email); + self::assertEquals('some-reference', $paypal->reference); + } + + /** @test */ + public function delete_hits_expected_uri(): void + { + $customerId = 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4'; + $paymentMethodId = 'paymtd_01hs8zx6x377xfsfrt2bqsevbw'; + $expectedUri = sprintf( + '%s/customers/%s/payment-methods/%s', + Environment::SANDBOX->baseUrl(), + $customerId, + $paymentMethodId, + ); + + $this->mockClient->addResponse(new Response(204)); + $this->client->paymentMethods->delete($customerId, $paymentMethodId); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('DELETE', $request->getMethod()); + self::assertEquals($expectedUri, urldecode((string) $request->getUri())); + } +} diff --git a/tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_card.json b/tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_card.json new file mode 100644 index 0000000..cab0d70 --- /dev/null +++ b/tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_card.json @@ -0,0 +1,22 @@ +{ + "data": { + "id": "paymtd_01hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": "0002", + "expiry_month": 1, + "expiry_year": 2025, + "cardholder_name": "Sam Miller" + }, + "paypal": null, + "origin": "subscription", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-04T11:50:23.422Z" + }, + "meta": { + "request_id": "03dae283-b7e9-47dc-b8c0-229576d90139" + } +} diff --git a/tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_paypal.json b/tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_paypal.json new file mode 100644 index 0000000..76260b5 --- /dev/null +++ b/tests/Functional/Resources/PaymentMethods/_fixtures/response/full_entity_paypal.json @@ -0,0 +1,19 @@ +{ + "data": { + "id": "paymtd_01hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "paypal", + "card": null, + "paypal": { + "email": "sam@example.com", + "reference": "some-reference" + }, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-04T11:50:23.422Z" + }, + "meta": { + "request_id": "03dae283-b7e9-47dc-b8c0-229576d90139" + } +} diff --git a/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_default.json b/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_default.json new file mode 100644 index 0000000..b799b25 --- /dev/null +++ b/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_default.json @@ -0,0 +1,47 @@ +{ + "data": [ + { + "id": "paymtd_01hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": "0002", + "expiry_month": 1, + "expiry_year": 2025 + }, + "cardholder_name": "Sam Miller", + "paypal": null, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-03T11:50:23.422Z" + }, + { + "id": "paymtd_02hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": "0002", + "expiry_month": 1, + "expiry_year": 2025 + }, + "cardholder_name": "Sam Miller", + "paypal": null, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-03T11:50:23.422Z" + } + ], + "meta": { + "request_id": "f831dd0b-150d-41c8-a952-5f8f84dbdcee", + "pagination": { + "per_page": 2, + "next": "https://api.paddle.com/customers/ctm_01h282ye3v2d9cmcm8dzpawrd0/payment-methods?after=paymtd_02hs8zx6x377xfsfrt2bqsevbw", + "has_more": false, + "estimated_total": 1 + } + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_one.json b/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_one.json new file mode 100644 index 0000000..003e199 --- /dev/null +++ b/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_one.json @@ -0,0 +1,47 @@ +{ + "data": [ + { + "id": "paymtd_01hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": "0002", + "expiry_month": 1, + "expiry_year": 2025 + }, + "cardholder_name": "Sam Miller", + "paypal": null, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-03T11:50:23.422Z" + }, + { + "id": "paymtd_02hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": "0002", + "expiry_month": 1, + "expiry_year": 2025 + }, + "cardholder_name": "Sam Miller", + "paypal": null, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-03T11:50:23.422Z" + } + ], + "meta": { + "request_id": "f831dd0b-150d-41c8-a952-5f8f84dbdcee", + "pagination": { + "per_page": 2, + "next": "https://sandbox-api.paddle.com/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?after=paymtd_02hs8zx6x377xfsfrt2bqsevbw", + "has_more": true, + "estimated_total": 4 + } + } +} diff --git a/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_two.json b/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_two.json new file mode 100644 index 0000000..e8a622f --- /dev/null +++ b/tests/Functional/Resources/PaymentMethods/_fixtures/response/list_paginated_page_two.json @@ -0,0 +1,47 @@ +{ + "data": [ + { + "id": "paymtd_03hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": "0002", + "expiry_month": 1, + "expiry_year": 2025 + }, + "cardholder_name": "Sam Miller", + "paypal": null, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-03T11:50:23.422Z" + }, + { + "id": "paymtd_04hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8h6jj90jjz0d71m6hj4r9z", + "type": "card", + "card": { + "type": "visa", + "last4": "0002", + "expiry_month": 1, + "expiry_year": 2025 + }, + "cardholder_name": "Sam Miller", + "paypal": null, + "origin": "saved_during_purchase", + "saved_at": "2024-05-03T11:50:23.422Z", + "updated_at": "2024-05-03T11:50:23.422Z" + } + ], + "meta": { + "request_id": "f831dd0b-150d-41c8-a952-5f8f84dbdcee", + "pagination": { + "per_page": 2, + "next": "https://sandbox-api.paddle.com/customers/ctm_01hv6y1jedq4p1n0yqn5ba3ky4/payment-methods?after=paymtd_04hs8zx6x377xfsfrt2bqsevbw", + "has_more": false, + "estimated_total": 4 + } + } +} diff --git a/tests/Unit/Entities/EventTest.php b/tests/Unit/Entities/EventTest.php index fc83893..ef4fb0a 100644 --- a/tests/Unit/Entities/EventTest.php +++ b/tests/Unit/Entities/EventTest.php @@ -5,7 +5,13 @@ namespace Paddle\SDK\Tests\Unit\Entities; use Paddle\SDK\Entities\Event; +use Paddle\SDK\Notifications\Entities\DeletedPaymentMethod; use Paddle\SDK\Notifications\Entities\Entity; +use Paddle\SDK\Notifications\Entities\PaymentMethod; +use Paddle\SDK\Notifications\Entities\Shared\SavedPaymentMethodDeletionReason; +use Paddle\SDK\Notifications\Entities\Shared\SavedPaymentMethodOrigin; +use Paddle\SDK\Notifications\Events\PaymentMethodDeleted; +use Paddle\SDK\Notifications\Events\PaymentMethodSaved; use Paddle\SDK\Tests\Utils\ReadsFixtures; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; @@ -153,6 +159,18 @@ public static function eventDataProvider(): iterable \Paddle\SDK\Notifications\Events\DiscountUpdated::class, \Paddle\SDK\Notifications\Entities\Discount::class, ], + [ + 'payment_method.saved', + 'paymentMethod', + PaymentMethodSaved::class, + PaymentMethod::class, + ], + [ + 'payment_method.deleted', + 'paymentMethod', + PaymentMethodDeleted::class, + DeletedPaymentMethod::class, + ], [ 'payout.created', 'payout', @@ -394,4 +412,71 @@ public function it_creates_from_request(): void self::assertSame('ntf_01h8bzam1z32agrxjwhjgqk8w6', $notification->notificationId); } + + /** + * @test + */ + public function it_creates_payment_method(): void + { + $event = Event::from([ + 'event_id' => 'evt_01h8bzakzx3hm2fmen703n5q45', + 'event_type' => 'payment_method.saved', + 'occurred_at' => '2023-08-21T11:57:47.390028Z', + 'notification_id' => 'ntf_01h8bzam1z32agrxjwhjgqk8w6', + 'data' => self::readJsonFixture('notification/entity/payment_method.saved'), + ]); + + self::assertSame('ntf_01h8bzam1z32agrxjwhjgqk8w6', $event->notificationId); + + self::assertInstanceOf(PaymentMethodSaved::class, $event); + self::assertInstanceOf(Entity::class, $event->data); + self::assertSame($event->data, $event->paymentMethod); + self::assertSame('evt_01h8bzakzx3hm2fmen703n5q45', $event->eventId); + self::assertSame('2023-08-21T11:57:47.390+00:00', $event->occurredAt->format(DATE_RFC3339_EXTENDED)); + self::assertSame('payment_method.saved', $event->eventType->getValue()); + + $paymentMethod = $event->paymentMethod; + self::assertInstanceOf(PaymentMethod::class, $paymentMethod); + + self::assertSame('paymtd_01hs8zx6x377xfsfrt2bqsevbw', $paymentMethod->id); + self::assertSame('ctm_01hv6y1jedq4p1n0yqn5ba3ky4', $paymentMethod->customerId); + self::assertSame('add_01hv8gq3318ktkfengj2r75gfx', $paymentMethod->addressId); + self::assertEquals(SavedPaymentMethodOrigin::SavedDuringPurchase(), $paymentMethod->origin); + self::assertSame('2024-05-02T02:55:25.198+00:00', $paymentMethod->savedAt->format(DATE_RFC3339_EXTENDED)); + self::assertSame('2024-05-02T02:55:25.198+00:00', $paymentMethod->updatedAt->format(DATE_RFC3339_EXTENDED)); + } + + /** + * @test + */ + public function it_creates_deleted_payment_method(): void + { + $event = Event::from([ + 'event_id' => 'evt_01h8bzakzx3hm2fmen703n5q45', + 'event_type' => 'payment_method.deleted', + 'occurred_at' => '2023-08-21T11:57:47.390028Z', + 'notification_id' => 'ntf_01h8bzam1z32agrxjwhjgqk8w6', + 'data' => self::readJsonFixture('notification/entity/payment_method.deleted'), + ]); + + self::assertSame('ntf_01h8bzam1z32agrxjwhjgqk8w6', $event->notificationId); + + self::assertInstanceOf(PaymentMethodDeleted::class, $event); + self::assertInstanceOf(Entity::class, $event->data); + self::assertSame($event->data, $event->paymentMethod); + self::assertSame('evt_01h8bzakzx3hm2fmen703n5q45', $event->eventId); + self::assertSame('2023-08-21T11:57:47.390+00:00', $event->occurredAt->format(DATE_RFC3339_EXTENDED)); + self::assertSame('payment_method.deleted', $event->eventType->getValue()); + + $paymentMethod = $event->paymentMethod; + self::assertInstanceOf(DeletedPaymentMethod::class, $paymentMethod); + + self::assertSame('paymtd_01hs8zx6x377xfsfrt2bqsevbw', $paymentMethod->id); + self::assertSame('ctm_01hv6y1jedq4p1n0yqn5ba3ky4', $paymentMethod->customerId); + self::assertSame('add_01hv8gq3318ktkfengj2r75gfx', $paymentMethod->addressId); + self::assertEquals(SavedPaymentMethodOrigin::SavedDuringPurchase(), $paymentMethod->origin); + self::assertEquals(SavedPaymentMethodDeletionReason::ReplacedByNewerVersion(), $paymentMethod->deletionReason); + self::assertSame('2024-05-02T02:55:25.198+00:00', $paymentMethod->savedAt->format(DATE_RFC3339_EXTENDED)); + self::assertSame('2024-05-03T12:24:18.826+00:00', $paymentMethod->updatedAt->format(DATE_RFC3339_EXTENDED)); + } } diff --git a/tests/Unit/Entities/_fixtures/notification/entity/payment_method.deleted.json b/tests/Unit/Entities/_fixtures/notification/entity/payment_method.deleted.json new file mode 100644 index 0000000..bca973a --- /dev/null +++ b/tests/Unit/Entities/_fixtures/notification/entity/payment_method.deleted.json @@ -0,0 +1,10 @@ +{ + "id": "paymtd_01hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8gq3318ktkfengj2r75gfx", + "deletion_reason": "replaced_by_newer_version", + "type": "card", + "origin": "saved_during_purchase", + "saved_at": "2024-05-02T02:55:25.198953Z", + "updated_at": "2024-05-03T12:24:18.826338Z" +} diff --git a/tests/Unit/Entities/_fixtures/notification/entity/payment_method.saved.json b/tests/Unit/Entities/_fixtures/notification/entity/payment_method.saved.json new file mode 100644 index 0000000..498939c --- /dev/null +++ b/tests/Unit/Entities/_fixtures/notification/entity/payment_method.saved.json @@ -0,0 +1,9 @@ +{ + "id": "paymtd_01hs8zx6x377xfsfrt2bqsevbw", + "customer_id": "ctm_01hv6y1jedq4p1n0yqn5ba3ky4", + "address_id": "add_01hv8gq3318ktkfengj2r75gfx", + "type": "card", + "origin": "saved_during_purchase", + "saved_at": "2024-05-02T02:55:25.198953Z", + "updated_at": "2024-05-02T02:55:25.198953Z" +}