From d2e683fe3a19b269276f54b64233654b77a97bea Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:49:23 -0700 Subject: [PATCH] feat: portal and embeddable sessions --- CHANGELOG.md | 6 + easypost/easypost_client.py | 4 + easypost/services/__init__.py | 2 + easypost/services/customer_portal_service.py | 23 +++ easypost/services/embeddable_service.py | 23 +++ ...t_customer_portal_account_link_create.yaml | 147 ++++++++++++++++++ .../test_embeddable_session_create.yaml | 143 +++++++++++++++++ .../test_referral_customer_create.yaml | 27 ++-- tests/conftest.py | 5 + tests/test_customer_portal.py | 15 ++ tests/test_embeddable.py | 13 ++ tests/test_referral_customer.py | 10 +- 12 files changed, 400 insertions(+), 18 deletions(-) create mode 100644 easypost/services/customer_portal_service.py create mode 100644 easypost/services/embeddable_service.py create mode 100644 tests/cassettes/test_customer_portal_account_link_create.yaml create mode 100644 tests/cassettes/test_embeddable_session_create.yaml create mode 100644 tests/test_customer_portal.py create mode 100644 tests/test_embeddable.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 95d0385d..dd755a74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## Next Release + +- Adds the following functions: + - `embeddable.create_session` + - `customer_portal.create_account_link` + ## v10.2.0 (2025-11-10) - Adds `UspsShipAccount` support to the create carrier method diff --git a/easypost/easypost_client.py b/easypost/easypost_client.py index b2c65ede..bbd0a6e7 100644 --- a/easypost/easypost_client.py +++ b/easypost/easypost_client.py @@ -19,8 +19,10 @@ CarrierAccountService, CarrierMetadataService, ClaimService, + CustomerPortalService, CustomsInfoService, CustomsItemService, + EmbeddableService, EndShipperService, EventService, InsuranceService, @@ -65,8 +67,10 @@ def __init__( self.carrier_account = CarrierAccountService(self) self.carrier_metadata = CarrierMetadataService(self) self.claim = ClaimService(self) + self.customer_portal = CustomerPortalService(self) self.customs_info = CustomsInfoService(self) self.customs_item = CustomsItemService(self) + self.embeddable = EmbeddableService(self) self.end_shipper = EndShipperService(self) self.event = EventService(self) self.insurance = InsuranceService(self) diff --git a/easypost/services/__init__.py b/easypost/services/__init__.py index 68b025d4..c792757b 100644 --- a/easypost/services/__init__.py +++ b/easypost/services/__init__.py @@ -8,8 +8,10 @@ from easypost.services.carrier_account_service import CarrierAccountService from easypost.services.carrier_metadata_service import CarrierMetadataService from easypost.services.claim_service import ClaimService +from easypost.services.customer_portal_service import CustomerPortalService from easypost.services.customs_info_service import CustomsInfoService from easypost.services.customs_item_service import CustomsItemService +from easypost.services.embeddable_service import EmbeddableService from easypost.services.end_shipper_service import EndShipperService from easypost.services.event_service import EventService from easypost.services.insurance_service import InsuranceService diff --git a/easypost/services/customer_portal_service.py b/easypost/services/customer_portal_service.py new file mode 100644 index 00000000..1e168e32 --- /dev/null +++ b/easypost/services/customer_portal_service.py @@ -0,0 +1,23 @@ +from typing import Any + +from easypost.easypost_object import convert_to_easypost_object +from easypost.requestor import ( + RequestMethod, + Requestor, +) +from easypost.services.base_service import BaseService + + +class CustomerPortalService(BaseService): + def __init__(self, client): + self._client = client + + def create_account_link(self, **params) -> dict[str, Any]: + """Create a Portal Session.""" + response = Requestor(self._client).request( + method=RequestMethod.POST, + url="/customer_portal/account_link", + params=params, + ) + + return convert_to_easypost_object(response=response) diff --git a/easypost/services/embeddable_service.py b/easypost/services/embeddable_service.py new file mode 100644 index 00000000..36205d4e --- /dev/null +++ b/easypost/services/embeddable_service.py @@ -0,0 +1,23 @@ +from typing import Any + +from easypost.easypost_object import convert_to_easypost_object +from easypost.requestor import ( + RequestMethod, + Requestor, +) +from easypost.services.base_service import BaseService + + +class EmbeddableService(BaseService): + def __init__(self, client): + self._client = client + + def create_session(self, **params) -> dict[str, Any]: + """Create an Embeddables Session.""" + response = Requestor(self._client).request( + method=RequestMethod.POST, + url="/embeddables/session", + params=params, + ) + + return convert_to_easypost_object(response=response) diff --git a/tests/cassettes/test_customer_portal_account_link_create.yaml b/tests/cassettes/test_customer_portal_account_link_create.yaml new file mode 100644 index 00000000..b40e880c --- /dev/null +++ b/tests/cassettes/test_customer_portal_account_link_create.yaml @@ -0,0 +1,147 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + authorization: + - + user-agent: + - + method: GET + uri: https://api.easypost.com/v2/users/children + response: + body: + string: '{"children": [{"id": "user_584be78af2f141e988b6c60dda9dd8fd", "object": + "User", "parent_id": "user_54356a6b96c940d8913961a3c7b909f2", "name": "Test + User2", "phone_number": "", "verified": true, "created_at": "2023-12-11T17:13:38Z"}, + {"id": "user_437e724f37de412db6df8821968d8d3c", "object": "User", "parent_id": + "user_54356a6b96c940d8913961a3c7b909f2", "name": "Test User2", "phone_number": + "", "verified": true, "created_at": "2023-12-11T17:13:43Z"}, {"id": + "user_2553102e90de43dea0b9fa7f8c8d7e33", "object": "User", "parent_id": "user_54356a6b96c940d8913961a3c7b909f2", + "name": "Child Account Name22", "phone_number": "", "verified": + true, "created_at": "2024-01-04T19:56:21Z"}, {"id": "user_31754439d4524fbb8867d138de537e12", + "object": "User", "parent_id": "user_54356a6b96c940d8913961a3c7b909f2", "name": + "Test User", "phone_number": "", "verified": true, "created_at": + "2024-01-19T21:42:21Z"}, {"id": "user_d26377e80676431d88c17d8b140d365a", "object": + "User", "parent_id": "user_54356a6b96c940d8913961a3c7b909f2", "name": "Test + User", "phone_number": "", "verified": true, "created_at": "2024-01-19T21:44:18Z"}], + "has_more": false}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '1114' + content-type: + - application/json; charset=utf-8 + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - 7265c3006917a058e786c0df01078abe + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb67nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb3nuq c0061e0a2e + - extlb1nuq cbbd141214 + x-runtime: + - '0.076112' + x-version-label: + - easypost-202511141214-a539f53042-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: '{"session_type": "account_onboarding", "user_id": "user_584be78af2f141e988b6c60dda9dd8fd", + "refresh_url": "https://example.com/refresh", "return_url": "https://example.com/return"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '180' + Content-Type: + - application/json + authorization: + - + user-agent: + - + method: POST + uri: https://api.easypost.com/v2/customer_portal/account_link + response: + body: + string: '{"link": "https://app.easypost.com/customer-portal/onboarding?session_id=nSsXPSJhZnqUzKUK", + "object": "CustomerPortalAccountLink", "expires_at": "2025-11-14T21:39:16Z", + "created_at": "2025-11-14T21:34:16Z"}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '199' + content-type: + - application/json; charset=utf-8 + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-backend: + - easypost + x-canary: + - direct + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - 7265c2fb6917a058e786c0e001078b74 + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb43nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb3nuq c0061e0a2e + - extlb1nuq cbbd141214 + x-runtime: + - '0.125907' + x-version-label: + - easypost-202511141214-a539f53042-master + x-xss-protection: + - 1; mode=block + status: + code: 201 + message: Created +version: 1 diff --git a/tests/cassettes/test_embeddable_session_create.yaml b/tests/cassettes/test_embeddable_session_create.yaml new file mode 100644 index 00000000..a292922f --- /dev/null +++ b/tests/cassettes/test_embeddable_session_create.yaml @@ -0,0 +1,143 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + authorization: + - + user-agent: + - + method: GET + uri: https://api.easypost.com/v2/users/children + response: + body: + string: '{"children": [{"id": "user_584be78af2f141e988b6c60dda9dd8fd", "object": + "User", "parent_id": "user_54356a6b96c940d8913961a3c7b909f2", "name": "Test + User2", "phone_number": "", "verified": true, "created_at": "2023-12-11T17:13:38Z"}, + {"id": "user_437e724f37de412db6df8821968d8d3c", "object": "User", "parent_id": + "user_54356a6b96c940d8913961a3c7b909f2", "name": "Test User2", "phone_number": + "", "verified": true, "created_at": "2023-12-11T17:13:43Z"}, {"id": + "user_2553102e90de43dea0b9fa7f8c8d7e33", "object": "User", "parent_id": "user_54356a6b96c940d8913961a3c7b909f2", + "name": "Child Account Name22", "phone_number": "", "verified": + true, "created_at": "2024-01-04T19:56:21Z"}, {"id": "user_31754439d4524fbb8867d138de537e12", + "object": "User", "parent_id": "user_54356a6b96c940d8913961a3c7b909f2", "name": + "Test User", "phone_number": "", "verified": true, "created_at": + "2024-01-19T21:42:21Z"}, {"id": "user_d26377e80676431d88c17d8b140d365a", "object": + "User", "parent_id": "user_54356a6b96c940d8913961a3c7b909f2", "name": "Test + User", "phone_number": "", "verified": true, "created_at": "2024-01-19T21:44:18Z"}], + "has_more": false}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '1114' + content-type: + - application/json; charset=utf-8 + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - 7265c2fe6917a363e786c4e3010cde14 + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb34nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb5nuq c0061e0a2e + - extlb1nuq cbbd141214 + x-runtime: + - '0.085273' + x-version-label: + - easypost-202511141214-a539f53042-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: '{"origin_host": "https://example.com", "user_id": "user_584be78af2f141e988b6c60dda9dd8fd"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '90' + Content-Type: + - application/json + authorization: + - + user-agent: + - + method: POST + uri: https://api.easypost.com/v2/embeddables/session + response: + body: + string: '{"object": "EmbeddablesSession", "session_id": "5iL-W-NePFb0FU_n", + "expires_at": "2025-11-14T22:02:15Z", "created_at": "2025-11-14T21:47:15Z"}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '135' + content-type: + - application/json; charset=utf-8 + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - 7265c2fc6917a363e786c4e4010cde8f + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb65nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb3nuq c0061e0a2e + - extlb1nuq cbbd141214 + x-runtime: + - '0.141938' + x-version-label: + - easypost-202511141214-a539f53042-master + x-xss-protection: + - 1; mode=block + status: + code: 201 + message: Created +version: 1 diff --git a/tests/cassettes/test_referral_customer_create.yaml b/tests/cassettes/test_referral_customer_create.yaml index 9fd00a90..c7c97f00 100644 --- a/tests/cassettes/test_referral_customer_create.yaml +++ b/tests/cassettes/test_referral_customer_create.yaml @@ -1,6 +1,7 @@ interactions: - request: - body: '{"user": {"name": "test test", "email": "test@test.com", "phone": "8888888888"}}' + body: '{"user": {"name": "Test Referral", "email": "test@example.com", "phone": + "5555555555"}}' headers: Accept: - '*/*' @@ -9,7 +10,7 @@ interactions: Connection: - keep-alive Content-Length: - - '80' + - '87' Content-Type: - application/json authorization: @@ -20,22 +21,22 @@ interactions: uri: https://api.easypost.com/v2/referral_customers response: body: - string: '{"id": "user_55b92a4a1633479488889ec5b1a2dac9", "object": "User", "parent_id": - null, "name": "test test", "phone_number": "", "verified": true, - "created_at": "2025-03-06T19:48:39Z", "default_carbon_offset": false, "has_elevate_access": + string: '{"id": "user_0857ace4c8cc4c1c820c0020f63b58e3", "object": "User", "parent_id": + null, "name": "Test Referral", "phone_number": "", "verified": true, + "created_at": "2025-11-14T21:41:14Z", "default_carbon_offset": false, "has_elevate_access": false, "balance": "0.00000", "price_per_shipment": "0.00000", "recharge_amount": null, "secondary_recharge_amount": null, "recharge_threshold": null, "has_billing_method": null, "cc_fee_rate": "0.0375", "default_insurance_amount": "50.00", "insurance_fee_rate": "0.01", "insurance_fee_minimum": "0.50", "email": "", "children": [], "api_keys": [{"object": "ApiKey", "key": "", "mode": "test", - "created_at": "2025-03-06T19:48:40Z", "active": true, "id": "ak_f490ecb3c0084330a8c518de543100bb"}, + "created_at": "2025-11-14T21:41:15Z", "active": true, "id": "ak_942665c2509a4d67af5d117a22ebbe2f"}, {"object": "ApiKey", "key": "", "mode": "production", "created_at": - "2025-03-06T19:48:40Z", "active": true, "id": "ak_6fc88d2d69464d0da46ed6d16cf65294"}]}' + "2025-11-14T21:41:15Z", "active": true, "id": "ak_6e11f073d4b74271b69ff9de29131033"}]}' headers: cache-control: - private, no-cache, no-store content-length: - - '1017' + - '1021' content-type: - application/json; charset=utf-8 expires: @@ -55,7 +56,7 @@ interactions: x-download-options: - noopen x-ep-request-uuid: - - 8e8eb62b67c9fc17e2b97b720018a5bb + - 7265c2fb6917a1fae786c164010a6c1a x-frame-options: - SAMEORIGIN x-node: @@ -63,12 +64,12 @@ interactions: x-permitted-cross-domain-policies: - none x-proxied: - - intlb4nuq 51d74985a2 - - extlb1nuq 99aac35317 + - intlb3nuq c0061e0a2e + - extlb1nuq cbbd141214 x-runtime: - - '0.598904' + - '0.677840' x-version-label: - - easypost-202503061913-8f39069a2d-master + - easypost-202511141214-a539f53042-master x-xss-protection: - 1; mode=block status: diff --git a/tests/conftest.py b/tests/conftest.py index f19cf250..1e7d5bea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -392,3 +392,8 @@ def luma_ruleset_name(): @pytest.fixture def luma_planned_ship_date(): return "2025-06-12" + + +@pytest.fixture +def referral_user(): + return read_fixture_data()["users"]["referral"] diff --git a/tests/test_customer_portal.py b/tests/test_customer_portal.py new file mode 100644 index 00000000..82e655b1 --- /dev/null +++ b/tests/test_customer_portal.py @@ -0,0 +1,15 @@ +import pytest + + +@pytest.mark.vcr() +def test_customer_portal_account_link_create(full_shipment, basic_claim, prod_client): + user = prod_client.user.all_children()["children"][0] + + account_link = prod_client.customer_portal.create_account_link( + session_type="account_onboarding", + user_id=user.id, + refresh_url="https://example.com/refresh", + return_url="https://example.com/return", + ) + + assert account_link.object == "CustomerPortalAccountLink" diff --git a/tests/test_embeddable.py b/tests/test_embeddable.py new file mode 100644 index 00000000..688370b6 --- /dev/null +++ b/tests/test_embeddable.py @@ -0,0 +1,13 @@ +import pytest + + +@pytest.mark.vcr() +def test_embeddable_session_create(full_shipment, basic_claim, prod_client): + user = prod_client.user.all_children()["children"][0] + + session = prod_client.embeddable.create_session( + origin_host="https://example.com", + user_id=user.id, + ) + + assert session.object == "EmbeddablesSession" diff --git a/tests/test_referral_customer.py b/tests/test_referral_customer.py index 968e5153..19305081 100644 --- a/tests/test_referral_customer.py +++ b/tests/test_referral_customer.py @@ -15,17 +15,17 @@ @pytest.mark.vcr() -def test_referral_customer_create(partner_user_prod_client): +def test_referral_customer_create(partner_user_prod_client, referral_user): """This test requires a partner customer's production API key via PARTNER_USER_PROD_API_KEY.""" created_referral_customer = partner_user_prod_client.referral_customer.create( - name="test test", - email="test@test.com", - phone="8888888888", + name=referral_user["name"], + email=referral_user["email"], + phone=referral_user["phone"], ) assert isinstance(created_referral_customer, User) assert str.startswith(created_referral_customer.id, "user_") - assert created_referral_customer.name == "test test" + assert created_referral_customer.name == "Test Referral" @pytest.mark.vcr()