Skip to content
Open
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: 3 additions & 4 deletions easypost/easypost_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,6 +22,7 @@
EmbeddableService,
EndShipperService,
EventService,
FedExRegistrationService,
InsuranceService,
LumaService,
OrderService,
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions easypost/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
103 changes: 103 additions & 0 deletions easypost/services/fedex_registration_service.py
Original file line number Diff line number Diff line change
@@ -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("-", "")
134 changes: 134 additions & 0 deletions tests/test_fedex_registration.py
Original file line number Diff line number Diff line change
@@ -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"