Skip to content

Commit

Permalink
feat: Add support for customer portal sessions (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidgrayston-paddle authored Dec 3, 2024
1 parent de2d28a commit 09e393c
Show file tree
Hide file tree
Showing 17 changed files with 315 additions and 0 deletions.
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"
)

0 comments on commit 09e393c

Please sign in to comment.