Skip to content
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
43 changes: 39 additions & 4 deletions plugins/easypost/karrio/providers/easypost/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
import karrio.core.models as models


# EasyPost API carrier name normalization mapping
# Maps EasyPost's carrier variations to Karrio's standard CarrierId enum names
CARRIER_NAME_NORMALIZATION = {
"UPSDAP": "UPS",
"UPS DAP": "UPS",
"FedExDefault": "FedEx",
"FedEx Default": "FedEx",
}


class LabelType(lib.Enum):
PDF = "PDF"
ZPL = "ZPL"
Expand Down Expand Up @@ -862,13 +872,38 @@ class Service(lib.StrEnum):

@staticmethod
def info(serviceName, carrier):
rate_provider = CarrierId.map(carrier).name_or_key
service = Service.map(serviceName)
# Normalize carrier name to handle EasyPost API variations
normalized_carrier = CARRIER_NAME_NORMALIZATION.get(carrier, carrier)
rate_provider = CarrierId.map(normalized_carrier).name_or_key

# Try carrier-qualified lookup first to avoid service name collisions
service_code = None
carrier_enum = CarrierId.map(normalized_carrier)

if carrier_enum.name and serviceName:
# Construct carrier-qualified service code: easypost_{carrier}_{service}
carrier_code = carrier_enum.name
service_snake = lib.to_snake_case(serviceName)
qualified_service_name = f"easypost_{carrier_code}_{service_snake}"

# Check if the qualified service exists
# Note: We cannot use Service.map() because Python enums with duplicate
# values create aliases, so Service.easypost_usps_priority and
# Service.easypost_canadapost_priority are the SAME object (both = 'Priority')
if hasattr(Service, qualified_service_name):
service_code = qualified_service_name

# Fallback to original behavior if qualified lookup fails
if not service_code:
service = Service.map(serviceName)
service_code = service.name_or_key

# Format the service name for display
service_name = re.sub(
r"((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))", r" \1", service.name_or_key
r"((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))", r" \1", service_code
).replace("easypost_", "")

return rate_provider, service.name_or_key, service_name
return rate_provider, service_code, service_name


class CarrierId(lib.StrEnum):
Expand Down
115 changes: 115 additions & 0 deletions plugins/easypost/tests/easypost/test_rate.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ def test_parse_error_response(self):

self.assertListEqual(DP.to_dict(parsed_response), ParsedErrorResponse)

def test_service_collision_fix(self):
"""Test that service collisions are resolved using carrier context"""
with patch("karrio.mappers.easypost.proxy.lib.request") as mock:
mock.return_value = ServiceCollisionResponseJSON
parsed_response = Rating.fetch(self.RateRequest).from_(gateway).parse()

self.assertListEqual(DP.to_dict(parsed_response), ParsedServiceCollisionResponse)


if __name__ == "__main__":
unittest.main()
Expand Down Expand Up @@ -303,3 +311,110 @@ def test_parse_error_response(self):
}
}
"""

# Test data for service collision fix
ServiceCollisionResponseJSON = """{
"id": "shp_test_collision",
"object": "Shipment",
"rates": [
{
"id": "rate_usps_priority",
"object": "Rate",
"carrier_account_id": "ca_usps",
"service": "Priority",
"rate": "15.85",
"carrier": "USPS",
"shipment_id": "shp_test_collision",
"delivery_days": 3,
"created_at": "2025-12-24T10:00:00Z",
"updated_at": "2025-12-24T10:00:00Z"
},
{
"id": "rate_ups_ground",
"object": "Rate",
"carrier_account_id": "ca_ups",
"service": "Ground",
"rate": "17.09",
"carrier": "UPS",
"shipment_id": "shp_test_collision",
"delivery_days": 3,
"created_at": "2025-12-24T10:00:00Z",
"updated_at": "2025-12-24T10:00:00Z"
},
{
"id": "rate_canadapost_priority",
"object": "Rate",
"carrier_account_id": "ca_canadapost",
"service": "Priority",
"rate": "18.50",
"carrier": "Canada Post",
"shipment_id": "shp_test_collision",
"delivery_days": 2,
"created_at": "2025-12-24T10:00:00Z",
"updated_at": "2025-12-24T10:00:00Z"
},
{
"id": "rate_canpar_ground",
"object": "Rate",
"carrier_account_id": "ca_canpar",
"service": "Ground",
"rate": "12.99",
"carrier": "Canpar",
"shipment_id": "shp_test_collision",
"delivery_days": 4,
"created_at": "2025-12-24T10:00:00Z",
"updated_at": "2025-12-24T10:00:00Z"
}
]
}
"""

ParsedServiceCollisionResponse = [
[
{
"carrier_id": "easypost",
"carrier_name": "easypost",
"meta": {
"rate_provider": "usps",
"service_name": "usps_priority",
},
"service": "easypost_usps_priority",
"total_charge": 15.85,
"transit_days": 3,
},
{
"carrier_id": "easypost",
"carrier_name": "easypost",
"meta": {
"rate_provider": "ups",
"service_name": "ups_ground",
},
"service": "easypost_ups_ground",
"total_charge": 17.09,
"transit_days": 3,
},
{
"carrier_id": "easypost",
"carrier_name": "easypost",
"meta": {
"rate_provider": "canadapost",
"service_name": "canadapost_priority",
},
"service": "easypost_canadapost_priority",
"total_charge": 18.5,
"transit_days": 2,
},
{
"carrier_id": "easypost",
"carrier_name": "easypost",
"meta": {
"rate_provider": "canpar",
"service_name": "canpar_ground",
},
"service": "easypost_canpar_ground",
"total_charge": 12.99,
"transit_days": 4,
},
],
[],
]