diff --git a/CHANGELOG.md b/CHANGELOG.md index c24d1c3..d625e28 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-python-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-python-sdk) + - `Client.customer_portal_sessions.create` + ## 1.1.2 - 2024-11-20 ### Fixed diff --git a/paddle_billing/Client.py b/paddle_billing/Client.py index ed5fe67..dd6745b 100644 --- a/paddle_billing/Client.py +++ b/paddle_billing/Client.py @@ -20,6 +20,7 @@ from paddle_billing.Resources.Adjustments.AdjustmentsClient import AdjustmentsClient from paddle_billing.Resources.Businesses.BusinessesClient import BusinessesClient from paddle_billing.Resources.Customers.CustomersClient import CustomersClient +from paddle_billing.Resources.CustomerPortalSessions.CustomerPortalSessionsClient import CustomerPortalSessionsClient from paddle_billing.Resources.Discounts.DiscountsClient import DiscountsClient from paddle_billing.Resources.Events.EventsClient import EventsClient from paddle_billing.Resources.EventTypes.EventTypesClient import EventTypesClient @@ -85,6 +86,7 @@ def __init__( self.adjustments = AdjustmentsClient(self) self.businesses = BusinessesClient(self) self.customers = CustomersClient(self) + self.customer_portal_sessions = CustomerPortalSessionsClient(self) self.discounts = DiscountsClient(self) self.events = EventsClient(self) self.event_types = EventTypesClient(self) diff --git a/paddle_billing/Entities/CustomerPortalSession.py b/paddle_billing/Entities/CustomerPortalSession.py new file mode 100644 index 0000000..ae9970e --- /dev/null +++ b/paddle_billing/Entities/CustomerPortalSession.py @@ -0,0 +1,23 @@ +from __future__ import annotations +from dataclasses import dataclass +from datetime import datetime + +from paddle_billing.Entities.Entity import Entity +from paddle_billing.Entities.CustomerPortalSessions import CustomerPortalSessionUrls + + +@dataclass +class CustomerPortalSession(Entity): + id: str + customer_id: str | None + urls: CustomerPortalSessionUrls + created_at: datetime + + @staticmethod + def from_dict(data: dict) -> CustomerPortalSession: + return CustomerPortalSession( + id=data["id"], + customer_id=data.get("customer_id"), + urls=CustomerPortalSessionUrls.from_dict(data["urls"]), + created_at=datetime.fromisoformat(data["created_at"]), + ) diff --git a/paddle_billing/Entities/CustomerPortalSessions/CustomerPortalSessionGeneralUrl.py b/paddle_billing/Entities/CustomerPortalSessions/CustomerPortalSessionGeneralUrl.py new file mode 100644 index 0000000..770ea0b --- /dev/null +++ b/paddle_billing/Entities/CustomerPortalSessions/CustomerPortalSessionGeneralUrl.py @@ -0,0 +1,13 @@ +from __future__ import annotations +from dataclasses import dataclass + + +@dataclass +class CustomerPortalSessionGeneralUrl: + overview: str + + @staticmethod + def from_dict(data: dict) -> CustomerPortalSessionGeneralUrl: + return CustomerPortalSessionGeneralUrl( + overview=data["overview"], + ) diff --git a/paddle_billing/Entities/CustomerPortalSessions/CustomerPortalSessionSubscriptionUrl.py b/paddle_billing/Entities/CustomerPortalSessions/CustomerPortalSessionSubscriptionUrl.py new file mode 100644 index 0000000..3713162 --- /dev/null +++ b/paddle_billing/Entities/CustomerPortalSessions/CustomerPortalSessionSubscriptionUrl.py @@ -0,0 +1,17 @@ +from __future__ import annotations +from dataclasses import dataclass + + +@dataclass +class CustomerPortalSessionSubscriptionUrl: + id: str + cancel_subscription: str + update_subscription_payment_method: str + + @staticmethod + def from_dict(data: dict) -> CustomerPortalSessionSubscriptionUrl: + return CustomerPortalSessionSubscriptionUrl( + id=data["id"], + cancel_subscription=data["cancel_subscription"], + update_subscription_payment_method=data["update_subscription_payment_method"], + ) diff --git a/paddle_billing/Entities/CustomerPortalSessions/CustomerPortalSessionUrls.py b/paddle_billing/Entities/CustomerPortalSessions/CustomerPortalSessionUrls.py new file mode 100644 index 0000000..de04edb --- /dev/null +++ b/paddle_billing/Entities/CustomerPortalSessions/CustomerPortalSessionUrls.py @@ -0,0 +1,24 @@ +from __future__ import annotations +from dataclasses import dataclass + +from paddle_billing.Entities.CustomerPortalSessions.CustomerPortalSessionGeneralUrl import ( + CustomerPortalSessionGeneralUrl, +) +from paddle_billing.Entities.CustomerPortalSessions.CustomerPortalSessionSubscriptionUrl import ( + CustomerPortalSessionSubscriptionUrl, +) + + +@dataclass +class CustomerPortalSessionUrls: + general: CustomerPortalSessionGeneralUrl + subscriptions: list[CustomerPortalSessionSubscriptionUrl] + + @staticmethod + def from_dict(data: dict) -> CustomerPortalSessionUrls: + return CustomerPortalSessionUrls( + general=CustomerPortalSessionGeneralUrl.from_dict(data["general"]), + subscriptions=[ + CustomerPortalSessionSubscriptionUrl.from_dict(item) for item in data.get("subscriptions", []) + ], + ) diff --git a/paddle_billing/Entities/CustomerPortalSessions/__init__.py b/paddle_billing/Entities/CustomerPortalSessions/__init__.py new file mode 100644 index 0000000..9b89501 --- /dev/null +++ b/paddle_billing/Entities/CustomerPortalSessions/__init__.py @@ -0,0 +1,7 @@ +from paddle_billing.Entities.CustomerPortalSessions.CustomerPortalSessionUrls import CustomerPortalSessionUrls +from paddle_billing.Entities.CustomerPortalSessions.CustomerPortalSessionGeneralUrl import ( + CustomerPortalSessionGeneralUrl, +) +from paddle_billing.Entities.CustomerPortalSessions.CustomerPortalSessionSubscriptionUrl import ( + CustomerPortalSessionSubscriptionUrl, +) diff --git a/paddle_billing/Resources/CustomerPortalSessions/CustomerPortalSessionsClient.py b/paddle_billing/Resources/CustomerPortalSessions/CustomerPortalSessionsClient.py new file mode 100644 index 0000000..d4c73cf --- /dev/null +++ b/paddle_billing/Resources/CustomerPortalSessions/CustomerPortalSessionsClient.py @@ -0,0 +1,24 @@ +from paddle_billing.ResponseParser import ResponseParser + +from paddle_billing.Entities.CustomerPortalSession import CustomerPortalSession + +from paddle_billing.Resources.CustomerPortalSessions.Operations import ( + CreateCustomerPortalSession, +) + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from paddle_billing.Client import Client + + +class CustomerPortalSessionsClient: + def __init__(self, client: "Client"): + self.client = client + self.response = None + + def create(self, customer_id: str, operation: CreateCustomerPortalSession) -> CustomerPortalSession: + self.response = self.client.post_raw(f"/customers/{customer_id}/portal-sessions", operation) + parser = ResponseParser(self.response) + + return CustomerPortalSession.from_dict(parser.get_data()) diff --git a/paddle_billing/Resources/CustomerPortalSessions/Operations/CreateCustomerPortalSession.py b/paddle_billing/Resources/CustomerPortalSessions/Operations/CreateCustomerPortalSession.py new file mode 100644 index 0000000..193a073 --- /dev/null +++ b/paddle_billing/Resources/CustomerPortalSessions/Operations/CreateCustomerPortalSession.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +from paddle_billing.Operation import Operation + + +@dataclass +class CreateCustomerPortalSession(Operation): + subscription_ids: list[str] = (None,) diff --git a/paddle_billing/Resources/CustomerPortalSessions/Operations/__init__.py b/paddle_billing/Resources/CustomerPortalSessions/Operations/__init__.py new file mode 100644 index 0000000..7b9aae6 --- /dev/null +++ b/paddle_billing/Resources/CustomerPortalSessions/Operations/__init__.py @@ -0,0 +1,3 @@ +from paddle_billing.Resources.CustomerPortalSessions.Operations.CreateCustomerPortalSession import ( + CreateCustomerPortalSession, +) diff --git a/paddle_billing/Resources/CustomerPortalSessions/__init__.py b/paddle_billing/Resources/CustomerPortalSessions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/Functional/Resources/CustomerPortalSessions/__init__.py b/tests/Functional/Resources/CustomerPortalSessions/__init__.py new file mode 100644 index 0000000..e69de29 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..d24a2e2 --- /dev/null +++ b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_multiple.json @@ -0,0 +1,6 @@ +{ + "subscription_ids": [ + "sub_01h04vsc0qhwtsbsxh3422wjs4", + "sub_02h04vsc0qhwtsbsxh3422wjs4" + ] +} \ No newline at end of file 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..8496ac3 --- /dev/null +++ b/tests/Functional/Resources/CustomerPortalSessions/_fixtures/request/create_single.json @@ -0,0 +1,5 @@ +{ + "subscription_ids": [ + "sub_01h04vsc0qhwtsbsxh3422wjs4" + ] +} \ No newline at end of file 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..4241aaa --- /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" + } +} \ No newline at end of file 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..f3ad388 --- /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" + } +} \ No newline at end of file diff --git a/tests/Functional/Resources/CustomerPortalSessions/test_CustomerPortalSessionsClient.py b/tests/Functional/Resources/CustomerPortalSessions/test_CustomerPortalSessionsClient.py new file mode 100644 index 0000000..064d688 --- /dev/null +++ b/tests/Functional/Resources/CustomerPortalSessions/test_CustomerPortalSessionsClient.py @@ -0,0 +1,127 @@ +from json import loads +from pytest import mark +from urllib.parse import unquote + +from paddle_billing.Entities.CustomerPortalSession import CustomerPortalSession +from paddle_billing.Entities.CustomerPortalSessions import ( + CustomerPortalSessionUrls, + CustomerPortalSessionGeneralUrl, + CustomerPortalSessionSubscriptionUrl, +) + +from paddle_billing.Resources.CustomerPortalSessions.Operations import CreateCustomerPortalSession + +from tests.Utils.ReadsFixture import ReadsFixtures + + +class TestAddressesClient: + @mark.parametrize( + "customer_id, operation, expected_request_body, expected_response_body, expected_path", + [ + ( + "ctm_01gysfvfy7vqhpzkq8rjmrq7an", + CreateCustomerPortalSession(["sub_01h04vsc0qhwtsbsxh3422wjs4"]), + ReadsFixtures.read_raw_json_fixture("request/create_single"), + ReadsFixtures.read_raw_json_fixture("response/full_entity_single"), + "/customers/ctm_01gysfvfy7vqhpzkq8rjmrq7an/portal-sessions", + ), + ( + "ctm_01gysfvfy7vqhpzkq8rjmrq7an", + CreateCustomerPortalSession(["sub_01h04vsc0qhwtsbsxh3422wjs4", "sub_02h04vsc0qhwtsbsxh3422wjs4"]), + ReadsFixtures.read_raw_json_fixture("request/create_multiple"), + ReadsFixtures.read_raw_json_fixture("response/full_entity_multiple"), + "/customers/ctm_01gysfvfy7vqhpzkq8rjmrq7an/portal-sessions", + ), + ], + ids=[ + "Create portal session with single subscription ID", + "Create portal session with multiple subscription IDs", + ], + ) + def test_create_uses_expected_payload( + self, + test_client, + mock_requests, + customer_id, + operation, + expected_request_body, + expected_response_body, + expected_path, + ): + expected_url = f"{test_client.base_url}{expected_path}" + mock_requests.post(expected_url, status_code=201, text=expected_response_body) + + response = test_client.client.customer_portal_sessions.create(customer_id, operation) + response_json = test_client.client.customer_portal_sessions.response.json() + request_json = test_client.client.payload + last_request = mock_requests.last_request + + assert isinstance(response, CustomerPortalSession) + assert last_request is not None + assert last_request.method == "POST" + assert test_client.client.status_code == 201 + assert ( + unquote(last_request.url) == expected_url + ), "The URL does not match the expected URL, verify the query string is correct" + assert loads(request_json) == loads( + expected_request_body + ), "The request JSON doesn't match the expected fixture JSON" + assert response_json == loads( + str(expected_response_body) + ), "The response JSON doesn't match the expected fixture JSON" + + def test_create_returns_expected_response( + self, + test_client, + mock_requests, + ): + customer_id = "ctm_01gysfvfy7vqhpzkq8rjmrq7an" + expected_path = "/customers/ctm_01gysfvfy7vqhpzkq8rjmrq7an/portal-sessions" + expected_response_body = ReadsFixtures.read_raw_json_fixture("response/full_entity_multiple") + + expected_url = f"{test_client.base_url}{expected_path}" + mock_requests.post(expected_url, status_code=201, text=expected_response_body) + + response = test_client.client.customer_portal_sessions.create( + customer_id, + CreateCustomerPortalSession(["sub_01h04vsc0qhwtsbsxh3422wjs4", "sub_02h04vsc0qhwtsbsxh3422wjs4"]), + ) + + assert isinstance(response, CustomerPortalSession) + assert response.id == "cpls_01h4ge9r64c22exjsx0fy8b48b" + assert response.customer_id == customer_id + assert response.created_at.isoformat() == "2024-10-25T06:53:58+00:00" + + urls = response.urls + assert isinstance(urls, CustomerPortalSessionUrls) + + general = urls.general + assert isinstance(general, CustomerPortalSessionGeneralUrl) + assert ( + general.overview + == "https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=overview&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g" + ) + + subscription1 = urls.subscriptions[0] + assert isinstance(subscription1, CustomerPortalSessionSubscriptionUrl) + assert subscription1.id == "sub_01h04vsc0qhwtsbsxh3422wjs4" + assert ( + subscription1.cancel_subscription + == "https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=cancel_subscription&subscription_id=sub_01h04vsc0qhwtsbsxh3422wjs4&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g" + ) + assert ( + subscription1.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" + ) + + subscription2 = urls.subscriptions[1] + assert isinstance(subscription2, CustomerPortalSessionSubscriptionUrl) + assert subscription2.id == "sub_02h04vsc0qhwtsbsxh3422wjs4" + assert ( + subscription2.cancel_subscription + == "https://customer-portal.paddle.com/cpl_01j7zbyqs3vah3aafp4jf62qaw?action=cancel_subscription&subscription_id=sub_02h04vsc0qhwtsbsxh3422wjs4&token=pga_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdG1fMDFncm5uNHp0YTVhMW1mMDJqanplN3kyeXMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE3Mjc2NzkyMzh9._oO12IejzdKmyKTwb7BLjmiILkx4_cSyGjXraOBUI_g" + ) + assert ( + subscription2.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" + )