diff --git a/easypost/easypost_client.py b/easypost/easypost_client.py index bbd0a6e..2da8503 100644 --- a/easypost/easypost_client.py +++ b/easypost/easypost_client.py @@ -5,10 +5,7 @@ SUPPORT_EMAIL, TIMEOUT, ) -from easypost.hooks import ( - RequestHook, - ResponseHook, -) +from easypost.hooks import RequestHook, ResponseHook from easypost.services import ( AddressService, ApiKeyService, @@ -25,6 +22,7 @@ EmbeddableService, EndShipperService, EventService, + FedExRegistrationService, InsuranceService, LumaService, OrderService, @@ -73,6 +71,7 @@ def __init__( self.embeddable = EmbeddableService(self) self.end_shipper = EndShipperService(self) self.event = EventService(self) + self.fedex_registration = FedExRegistrationService(self) self.insurance = InsuranceService(self) self.luma = LumaService(self) self.order = OrderService(self) diff --git a/easypost/services/__init__.py b/easypost/services/__init__.py index c792757..61385d1 100644 --- a/easypost/services/__init__.py +++ b/easypost/services/__init__.py @@ -14,6 +14,7 @@ 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.fedex_registration_service import FedExRegistrationService from easypost.services.insurance_service import InsuranceService from easypost.services.luma_service import LumaService from easypost.services.order_service import OrderService diff --git a/easypost/services/fedex_registration_service.py b/easypost/services/fedex_registration_service.py new file mode 100644 index 0000000..7406246 --- /dev/null +++ b/easypost/services/fedex_registration_service.py @@ -0,0 +1,103 @@ +import uuid +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 FedExRegistrationService(BaseService): + def __init__(self, client): + self._client = client + + def register_address(self, fedex_account_number: str, **params) -> dict[str, Any]: + """Register the billing address for a FedEx account.""" + wrapped_params = self._wrap_address_validation(params) + url = f"/fedex_registrations/{fedex_account_number}/address" + + response = Requestor(self._client).request(method=RequestMethod.POST, url=url, params=wrapped_params) + + return convert_to_easypost_object(response=response) + + def request_pin(self, fedex_account_number: str, pin_method_option: str) -> dict[str, Any]: + """Request a PIN for FedEx account verification.""" + wrapped_params = {"pin_method": {"option": pin_method_option}} + url = f"/fedex_registrations/{fedex_account_number}/pin" + + response = Requestor(self._client).request(method=RequestMethod.POST, url=url, params=wrapped_params) + + return convert_to_easypost_object(response=response) + + def validate_pin(self, fedex_account_number: str, **params) -> dict[str, Any]: + """Validate the PIN entered by the user for FedEx account verification.""" + wrapped_params = self._wrap_pin_validation(params) + url = f"/fedex_registrations/{fedex_account_number}/pin/validate" + + response = Requestor(self._client).request(method=RequestMethod.POST, url=url, params=wrapped_params) + + return convert_to_easypost_object(response=response) + + def submit_invoice(self, fedex_account_number: str, **params) -> dict[str, Any]: + """Submit invoice information to complete FedEx account registration.""" + wrapped_params = self._wrap_invoice_validation(params) + url = f"/fedex_registrations/{fedex_account_number}/invoice" + + response = Requestor(self._client).request(method=RequestMethod.POST, url=url, params=wrapped_params) + + return convert_to_easypost_object(response=response) + + def _wrap_address_validation(self, params: dict[str, Any]) -> dict[str, Any]: + """Wraps address validation parameters and ensures the "name" field exists. + If not present, generates a UUID (with hyphens removed) as the name. + """ + wrapped_params = {} + + if "address_validation" in params: + address_validation = params["address_validation"].copy() + self._ensure_name_field(address_validation) + wrapped_params["address_validation"] = address_validation + + if "easypost_details" in params: + wrapped_params["easypost_details"] = params["easypost_details"] + + return wrapped_params + + def _wrap_pin_validation(self, params: dict[str, Any]) -> dict[str, Any]: + """Wraps PIN validation parameters and ensures the "name" field exists. + If not present, generates a UUID (with hyphens removed) as the name. + """ + wrapped_params = {} + + if "pin_validation" in params: + pin_validation = params["pin_validation"].copy() + self._ensure_name_field(pin_validation) + wrapped_params["pin_validation"] = pin_validation + + if "easypost_details" in params: + wrapped_params["easypost_details"] = params["easypost_details"] + + return wrapped_params + + def _wrap_invoice_validation(self, params: dict[str, Any]) -> dict[str, Any]: + """Wraps invoice validation parameters and ensures the "name" field exists. + If not present, generates a UUID (with hyphens removed) as the name. + """ + wrapped_params = {} + + if "invoice_validation" in params: + invoice_validation = params["invoice_validation"].copy() + self._ensure_name_field(invoice_validation) + wrapped_params["invoice_validation"] = invoice_validation + + if "easypost_details" in params: + wrapped_params["easypost_details"] = params["easypost_details"] + + return wrapped_params + + def _ensure_name_field(self, mapping: dict[str, Any]) -> None: + """Ensures the "name" field exists in the provided map. + If not present, generates a UUID (with hyphens removed) as the name. + This follows the pattern used in the web UI implementation. + """ + if "name" not in mapping or mapping["name"] is None: + mapping["name"] = str(uuid.uuid4()).replace("-", "") diff --git a/tests/test_fedex_registration.py b/tests/test_fedex_registration.py new file mode 100644 index 0000000..c37d07c --- /dev/null +++ b/tests/test_fedex_registration.py @@ -0,0 +1,134 @@ +from unittest.mock import MagicMock + +from easypost.easypost_object import EasyPostObject +from easypost.models import CarrierAccount + + +def test_register_address(prod_client, monkeypatch): + """Tests registering a billing address.""" + fedex_account_number = "123456789" + address_validation = { + "name": "BILLING NAME", + "street1": "1234 BILLING STREET", + "city": "BILLINGCITY", + "state": "ST", + "postal_code": "12345", + "country_code": "US", + } + easypost_details = {"carrier_account_id": "ca_123"} + params = { + "address_validation": address_validation, + "easypost_details": easypost_details, + } + + json_response = { + "email_address": None, + "options": ["SMS", "CALL", "INVOICE"], + "phone_number": "***-***-9721", + } + + mock_requestor = MagicMock() + mock_requestor.return_value.request.return_value = json_response + + monkeypatch.setattr("easypost.services.fedex_registration_service.Requestor", mock_requestor) + + response = prod_client.fedex_registration.register_address(fedex_account_number, **params) + assert isinstance(response, EasyPostObject) + assert response.email_address is None + assert "SMS" in response.options + assert "CALL" in response.options + assert "INVOICE" in response.options + assert response.phone_number == "***-***-9721" + + +def test_request_pin(prod_client, monkeypatch): + """Tests requesting a pin.""" + fedex_account_number = "123456789" + + json_response = {"message": "sent secured Pin"} + + mock_requestor = MagicMock() + mock_requestor.return_value.request.return_value = json_response + + monkeypatch.setattr("easypost.services.fedex_registration_service.Requestor", mock_requestor) + + response = prod_client.fedex_registration.request_pin(fedex_account_number, "SMS") + assert isinstance(response, EasyPostObject) + assert response.message == "sent secured Pin" + + +def test_validate_pin(prod_client, monkeypatch): + """Tests validating a pin.""" + fedex_account_number = "123456789" + pin_validation = { + "pin_code": "123456", + "name": "BILLING NAME", + } + easypost_details = {"carrier_account_id": "ca_123"} + params = { + "pin_validation": pin_validation, + "easypost_details": easypost_details, + } + + json_response = { + "id": "ca_123", + "object": "CarrierAccount", + "type": "FedexAccount", + "credentials": { + "account_number": "123456789", + "mfa_key": "123456789-XXXXX", + }, + } + + mock_requestor = MagicMock() + mock_requestor.return_value.request.return_value = json_response + + monkeypatch.setattr("easypost.services.fedex_registration_service.Requestor", mock_requestor) + + response = prod_client.fedex_registration.validate_pin(fedex_account_number, **params) + assert isinstance(response, CarrierAccount) + assert response.id == "ca_123" + assert response.object == "CarrierAccount" + assert response.type == "FedexAccount" + assert response.credentials["account_number"] == "123456789" + assert response.credentials["mfa_key"] == "123456789-XXXXX" + + +def test_submit_invoice(prod_client, monkeypatch): + """Tests submitting details about an invoice.""" + fedex_account_number = "123456789" + invoice_validation = { + "name": "BILLING NAME", + "invoice_number": "INV-12345", + "invoice_date": "2025-12-08", + "invoice_amount": "100.00", + "invoice_currency": "USD", + } + easypost_details = {"carrier_account_id": "ca_123"} + params = { + "invoice_validation": invoice_validation, + "easypost_details": easypost_details, + } + + json_response = { + "id": "ca_123", + "object": "CarrierAccount", + "type": "FedexAccount", + "credentials": { + "account_number": "123456789", + "mfa_key": "123456789-XXXXX", + }, + } + + mock_requestor = MagicMock() + mock_requestor.return_value.request.return_value = json_response + + monkeypatch.setattr("easypost.services.fedex_registration_service.Requestor", mock_requestor) + + response = prod_client.fedex_registration.submit_invoice(fedex_account_number, **params) + assert isinstance(response, CarrierAccount) + assert response.id == "ca_123" + assert response.object == "CarrierAccount" + assert response.type == "FedexAccount" + assert response.credentials["account_number"] == "123456789" + assert response.credentials["mfa_key"] == "123456789-XXXXX"