Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for customer portal sessions #79

Merged
merged 1 commit into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions paddle_billing/Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions paddle_billing/Entities/CustomerPortalSession.py
Original file line number Diff line number Diff line change
@@ -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"]),
)
Original file line number Diff line number Diff line change
@@ -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"],
)
Original file line number Diff line number Diff line change
@@ -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"],
)
Original file line number Diff line number Diff line change
@@ -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", [])
],
)
7 changes: 7 additions & 0 deletions paddle_billing/Entities/CustomerPortalSessions/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -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())
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from dataclasses import dataclass

from paddle_billing.Operation import Operation


@dataclass
class CreateCustomerPortalSession(Operation):
subscription_ids: list[str] = (None,)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from paddle_billing.Resources.CustomerPortalSessions.Operations.CreateCustomerPortalSession import (
CreateCustomerPortalSession,
)
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"subscription_ids": [
"sub_01h04vsc0qhwtsbsxh3422wjs4",
"sub_02h04vsc0qhwtsbsxh3422wjs4"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"subscription_ids": [
"sub_01h04vsc0qhwtsbsxh3422wjs4"
]
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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"
)
Loading