From 23716aab963cf459b97013a8287b23d96d3a0be7 Mon Sep 17 00:00:00 2001 From: David Grayston Date: Tue, 3 Dec 2024 19:47:09 +0000 Subject: [PATCH] feat: Add support for customer portal sessions --- CHANGELOG.md | 7 + src/Client.php | 3 + src/Entities/CustomerPortalSession.php | 36 +++++ .../CustomerPortalSessionGeneralUrl.php | 29 ++++ .../CustomerPortalSessionSubscriptionUrl.php | 33 +++++ .../CustomerPortalSessionUrls.php | 37 +++++ .../CustomerPortalSessionsClient.php | 40 ++++++ .../CreateCustomerPortalSession.php | 28 ++++ .../CustomerPortalSessionsClientTest.php | 130 ++++++++++++++++++ .../_fixtures/request/create_empty.json | 3 + .../_fixtures/request/create_multiple.json | 6 + .../_fixtures/request/create_single.json | 5 + .../_fixtures/response/full_entity_empty.json | 16 +++ .../response/full_entity_multiple.json | 27 ++++ .../response/full_entity_single.json | 22 +++ 15 files changed, 422 insertions(+) create mode 100644 src/Entities/CustomerPortalSession.php create mode 100644 src/Entities/CustomerPortalSession/CustomerPortalSessionGeneralUrl.php create mode 100644 src/Entities/CustomerPortalSession/CustomerPortalSessionSubscriptionUrl.php create mode 100644 src/Entities/CustomerPortalSession/CustomerPortalSessionUrls.php create mode 100644 src/Resources/CustomerPortalSessions/CustomerPortalSessionsClient.php create mode 100644 src/Resources/CustomerPortalSessions/Operations/CreateCustomerPortalSession.php create mode 100644 tests/Functional/Resources/CustomerPortalSessions/CustomerPortalSessionsClientTest.php create mode 100644 tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_empty.json create mode 100644 tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_multiple.json create mode 100644 tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_single.json create mode 100644 tests/Functional/Resources/CustomerPortalSessions/_fixtures/response/full_entity_empty.json create mode 100644 tests/Functional/Resources/CustomerPortalSessions/_fixtures/response/full_entity_multiple.json create mode 100644 tests/Functional/Resources/CustomerPortalSessions/_fixtures/response/full_entity_single.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ca25e65..63d1bb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ 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 customer portal sessions, see [related changelog](https://developer.paddle.com/changelog/2024/customer-portal-sessions?utm_source=dx&utm_medium=paddle-php-sdk) + - `Client->customerPortalSessions->create()` + ## [1.5.0] - 2024-11-18 ### Added diff --git a/src/Client.php b/src/Client.php index d59b177..95c2ff2 100644 --- a/src/Client.php +++ b/src/Client.php @@ -22,6 +22,7 @@ use Paddle\SDK\Resources\Addresses\AddressesClient; use Paddle\SDK\Resources\Adjustments\AdjustmentsClient; use Paddle\SDK\Resources\Businesses\BusinessesClient; +use Paddle\SDK\Resources\CustomerPortalSessions\CustomerPortalSessionsClient; use Paddle\SDK\Resources\Customers\CustomersClient; use Paddle\SDK\Resources\Discounts\DiscountsClient; use Paddle\SDK\Resources\Events\EventsClient; @@ -69,6 +70,7 @@ class Client public readonly TransactionsClient $transactions; public readonly AdjustmentsClient $adjustments; public readonly CustomersClient $customers; + public readonly CustomerPortalSessionsClient $customerPortalSessions; public readonly AddressesClient $addresses; public readonly BusinessesClient $businesses; public readonly DiscountsClient $discounts; @@ -118,6 +120,7 @@ public function __construct( $this->adjustments = new AdjustmentsClient($this); $this->customers = new CustomersClient($this); $this->addresses = new AddressesClient($this); + $this->customerPortalSessions = new CustomerPortalSessionsClient($this); $this->businesses = new BusinessesClient($this); $this->discounts = new DiscountsClient($this); $this->subscriptions = new SubscriptionsClient($this); diff --git a/src/Entities/CustomerPortalSession.php b/src/Entities/CustomerPortalSession.php new file mode 100644 index 0000000..43b1069 --- /dev/null +++ b/src/Entities/CustomerPortalSession.php @@ -0,0 +1,36 @@ + CustomerPortalSessionSubscriptionUrl::from($item), + $data['subscriptions'] ?? [], + ), + ); + } +} diff --git a/src/Resources/CustomerPortalSessions/CustomerPortalSessionsClient.php b/src/Resources/CustomerPortalSessions/CustomerPortalSessionsClient.php new file mode 100644 index 0000000..56f8b7b --- /dev/null +++ b/src/Resources/CustomerPortalSessions/CustomerPortalSessionsClient.php @@ -0,0 +1,40 @@ +client->postRaw("/customers/{$customerId}/portal-sessions", $createOperation), + ); + + return CustomerPortalSession::from($parser->getData()); + } +} diff --git a/src/Resources/CustomerPortalSessions/Operations/CreateCustomerPortalSession.php b/src/Resources/CustomerPortalSessions/Operations/CreateCustomerPortalSession.php new file mode 100644 index 0000000..8985488 --- /dev/null +++ b/src/Resources/CustomerPortalSessions/Operations/CreateCustomerPortalSession.php @@ -0,0 +1,28 @@ +filterUndefined([ + 'subscription_ids' => $this->subscriptionIds, + ]); + } +} diff --git a/tests/Functional/Resources/CustomerPortalSessions/CustomerPortalSessionsClientTest.php b/tests/Functional/Resources/CustomerPortalSessions/CustomerPortalSessionsClientTest.php new file mode 100644 index 0000000..ad8523e --- /dev/null +++ b/tests/Functional/Resources/CustomerPortalSessions/CustomerPortalSessionsClientTest.php @@ -0,0 +1,130 @@ +mockClient = new MockClient(); + $this->client = new Client( + apiKey: 'API_KEY_PLACEHOLDER', + options: new Options(Environment::SANDBOX), + httpClient: $this->mockClient); + } + + /** + * @test + * + * @dataProvider createOperationsProvider + */ + public function it_uses_expected_payload_on_create( + CreateCustomerPortalSession $operation, + ResponseInterface $response, + string $expectedBody, + ): void { + $this->mockClient->addResponse($response); + $this->client->customerPortalSessions->create('ctm_01h844p3h41s12zs5mn4axja51', $operation); + $request = $this->mockClient->getLastRequest(); + + self::assertInstanceOf(RequestInterface::class, $request); + self::assertEquals('POST', $request->getMethod()); + self::assertEquals( + Environment::SANDBOX->baseUrl() . '/customers/ctm_01h844p3h41s12zs5mn4axja51/portal-sessions', + urldecode((string) $request->getUri()), + ); + self::assertJsonStringEqualsJsonString($expectedBody, (string) $request->getBody()); + } + + public static function createOperationsProvider(): \Generator + { + yield 'Create portal session with single subscription ID' => [ + new CreateCustomerPortalSession(['sub_01h04vsc0qhwtsbsxh3422wjs4']), + new Response(201, body: self::readRawJsonFixture('response/full_entity_single')), + self::readRawJsonFixture('request/create_single'), + ]; + + yield 'Create portal session with multiple subscription IDs' => [ + new CreateCustomerPortalSession(['sub_01h04vsc0qhwtsbsxh3422wjs4', 'sub_02h04vsc0qhwtsbsxh3422wjs4']), + new Response(201, body: self::readRawJsonFixture('response/full_entity_multiple')), + self::readRawJsonFixture('request/create_multiple'), + ]; + + yield 'Create portal session with empty subscription IDs' => [ + new CreateCustomerPortalSession([]), + new Response(201, body: self::readRawJsonFixture('response/full_entity_empty')), + self::readRawJsonFixture('request/create_empty'), + ]; + + yield 'Create portal session with omitted subscription IDs' => [ + new CreateCustomerPortalSession(), + new Response(201, body: self::readRawJsonFixture('response/full_entity_empty')), + '{}', + ]; + } + + /** + * @test + */ + public function it_returns_expected_response_on_create(): void + { + $operation = new CreateCustomerPortalSession(['sub_01h04vsc0qhwtsbsxh3422wjs4', 'sub_02h04vsc0qhwtsbsxh3422wjs4']); + $response = new Response(201, body: self::readRawJsonFixture('response/full_entity_multiple')); + + $this->mockClient->addResponse($response); + $portalSession = $this->client->customerPortalSessions->create('ctm_01gysfvfy7vqhpzkq8rjmrq7an', $operation); + + self::assertEquals('cpls_01h4ge9r64c22exjsx0fy8b48b', $portalSession->id); + self::assertEquals('ctm_01gysfvfy7vqhpzkq8rjmrq7an', $portalSession->customerId); + self::assertEquals('2024-10-25T06:53:58+00:00', $portalSession->createdAt->format(DATE_RFC3339)); + + self::assertEquals( + 'https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=overview&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g', + $portalSession->urls->general->overview, + ); + + self::assertEquals( + 'sub_01h04vsc0qhwtsbsxh3422wjs4', + $portalSession->urls->subscriptions[0]->id, + ); + self::assertEquals( + 'https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=cancel_subscription&subscription_id=sub_01h04vsc0qhwtsbsxh3422wjs4&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g', + $portalSession->urls->subscriptions[0]->cancelSubscription, + ); + self::assertEquals( + 'https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=update_subscription_payment_method&subscription_id=sub_01h04vsc0qhwtsbsxh3422wjs4&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g', + $portalSession->urls->subscriptions[0]->updateSubscriptionPaymentMethod, + ); + + self::assertEquals( + 'sub_02h04vsc0qhwtsbsxh3422wjs4', + $portalSession->urls->subscriptions[1]->id, + ); + self::assertEquals( + 'https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=cancel_subscription&subscription_id=sub_02h04vsc0qhwtsbsxh3422wjs4&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g', + $portalSession->urls->subscriptions[1]->cancelSubscription, + ); + self::assertEquals( + 'https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=update_subscription_payment_method&subscription_id=sub_02h04vsc0qhwtsbsxh3422wjs4&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g', + $portalSession->urls->subscriptions[1]->updateSubscriptionPaymentMethod, + ); + } +} diff --git a/tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_empty.json b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_empty.json new file mode 100644 index 0000000..3884dfd --- /dev/null +++ b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_empty.json @@ -0,0 +1,3 @@ +{ + "subscription_ids": [] +} diff --git a/tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_multiple.json b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_multiple.json new file mode 100644 index 0000000..1c9ff7b --- /dev/null +++ b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_multiple.json @@ -0,0 +1,6 @@ +{ + "subscription_ids": [ + "sub_01h04vsc0qhwtsbsxh3422wjs4", + "sub_02h04vsc0qhwtsbsxh3422wjs4" + ] +} diff --git a/tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_single.json b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_single.json new file mode 100644 index 0000000..b9039c8 --- /dev/null +++ b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_single.json @@ -0,0 +1,5 @@ +{ + "subscription_ids": [ + "sub_01h04vsc0qhwtsbsxh3422wjs4" + ] +} diff --git a/tests/Functional/Resources/CustomerPortalSessions/_fixtures/response/full_entity_empty.json b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/response/full_entity_empty.json new file mode 100644 index 0000000..6c8fe80 --- /dev/null +++ b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/response/full_entity_empty.json @@ -0,0 +1,16 @@ +{ + "data": { + "id": "cpls_01h4ge9r64c22exjsx0fy8b48b", + "customer_id": "ctm_01gysfvfy7vqhpzkq8rjmrq7an", + "urls": { + "general": { + "overview": "https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=overview&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g" + }, + "subscriptions": [] + }, + "created_at": "2024-10-25T06:53:58Z" + }, + "meta": { + "request_id": "fa176777-4bca-49ec-aa1e-f53885333cb7" + } +} diff --git a/tests/Functional/Resources/CustomerPortalSessions/_fixtures/response/full_entity_multiple.json b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/response/full_entity_multiple.json new file mode 100644 index 0000000..9168698 --- /dev/null +++ b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/response/full_entity_multiple.json @@ -0,0 +1,27 @@ +{ + "data": { + "id": "cpls_01h4ge9r64c22exjsx0fy8b48b", + "customer_id": "ctm_01gysfvfy7vqhpzkq8rjmrq7an", + "urls": { + "general": { + "overview": "https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=overview&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g" + }, + "subscriptions": [ + { + "id": "sub_01h04vsc0qhwtsbsxh3422wjs4", + "cancel_subscription": "https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=cancel_subscription&subscription_id=sub_01h04vsc0qhwtsbsxh3422wjs4&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g", + "update_subscription_payment_method": "https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=update_subscription_payment_method&subscription_id=sub_01h04vsc0qhwtsbsxh3422wjs4&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g" + }, + { + "id": "sub_02h04vsc0qhwtsbsxh3422wjs4", + "cancel_subscription": "https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=cancel_subscription&subscription_id=sub_02h04vsc0qhwtsbsxh3422wjs4&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g", + "update_subscription_payment_method": "https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=update_subscription_payment_method&subscription_id=sub_02h04vsc0qhwtsbsxh3422wjs4&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g" + } + ] + }, + "created_at": "2024-10-25T06:53:58Z" + }, + "meta": { + "request_id": "fa176777-4bca-49ec-aa1e-f53885333cb7" + } +} diff --git a/tests/Functional/Resources/CustomerPortalSessions/_fixtures/response/full_entity_single.json b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/response/full_entity_single.json new file mode 100644 index 0000000..5718df2 --- /dev/null +++ b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/response/full_entity_single.json @@ -0,0 +1,22 @@ +{ + "data": { + "id": "cpls_01h4ge9r64c22exjsx0fy8b48b", + "customer_id": "ctm_01gysfvfy7vqhpzkq8rjmrq7an", + "urls": { + "general": { + "overview": "https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=overview&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g" + }, + "subscriptions": [ + { + "id": "sub_01h04vsc0qhwtsbsxh3422wjs4", + "cancel_subscription": "https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=cancel_subscription&subscription_id=sub_01h04vsc0qhwtsbsxh3422wjs4&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g", + "update_subscription_payment_method": "https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=update_subscription_payment_method&subscription_id=sub_01h04vsc0qhwtsbsxh3422wjs4&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g" + } + ] + }, + "created_at": "2024-10-25T06:53:58Z" + }, + "meta": { + "request_id": "fa176777-4bca-49ec-aa1e-f53885333cb7" + } +}