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
26 changes: 16 additions & 10 deletions modules/connectors/fedex/karrio/providers/fedex/pickup/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
import karrio.providers.fedex.error as error
import karrio.providers.fedex.utils as provider_utils
import karrio.providers.fedex.units as provider_units
from karrio.providers.fedex.pickup.utils import (
validate_package_location,
validate_pickup_address_type,
resolve_notification_emails,
build_notification_email_details,
)


def parse_pickup_response(
Expand Down Expand Up @@ -45,7 +51,9 @@ def pickup_request(
payload: models.PickupRequest,
settings: provider_utils.Settings,
) -> lib.Serializable:
package_location = validate_package_location(payload.package_location)
address = lib.to_address(payload.address)
notification_emails = resolve_notification_emails(address.email, payload.options)
packages = lib.to_packages(payload.parcels)
options = lib.units.Options(
payload.options,
Expand All @@ -67,6 +75,9 @@ def pickup_request(
# fmt: on
),
)
pickup_address_type = validate_pickup_address_type(
options.fedex_pickup_address_type.state
)

# Map unified pickup_type to FedEx pickup type
# one_time -> ON_CALL, daily/recurring -> REGULAR_STOP
Expand All @@ -87,7 +98,7 @@ def pickup_request(
value=settings.account_number,
),
originDetail=fedex.OriginDetailType(
pickupAddressType=options.fedex_pickup_address_type.state,
pickupAddressType=pickup_address_type,
pickupLocation=fedex.PickupLocationType(
contact=fedex.ContactType(
companyName=address.company_name,
Expand All @@ -113,7 +124,7 @@ def pickup_request(
readyDateTimestamp=f"{payload.pickup_date}T{ready_time}:00Z",
customerCloseTime=f"{closing_time}:00",
pickupDateType=options.fedex_pickup_date_type.state,
packageLocation=payload.package_location,
packageLocation=package_location,
buildingPart=options.fedex_building_part.state,
buildingPartDescription=options.fedex_building_part_description.state,
earlyPickup=options.fedex_early_pickup.state,
Expand All @@ -128,7 +139,7 @@ def pickup_request(
packageCount=len(packages),
carrierCode=options.fedex_carrier_code.state or "FDXE",
accountAddressOfRecord=None,
remarks=None,
remarks=payload.instruction,
countryRelationships=None,
pickupType=fedex_pickup_type,
trackingNumber=None,
Expand All @@ -137,16 +148,11 @@ def pickup_request(
oversizePackageCount=None,
pickupNotificationDetail=lib.identity(
fedex.PickupNotificationDetailType(
emailDetails=[
fedex.EmailDetailType(
address=address.email,
locale="en_US",
)
],
emailDetails=build_notification_email_details(notification_emails),
format="TEXT",
userMessage=options.fedex_user_message.state,
)
if address.email
if any(notification_emails)
else None
),
)
Expand Down
24 changes: 15 additions & 9 deletions modules/connectors/fedex/karrio/providers/fedex/pickup/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
import karrio.providers.fedex.utils as provider_utils
import karrio.providers.fedex.units as provider_units
import karrio.providers.fedex.pickup.cancel as cancel
from karrio.providers.fedex.pickup.utils import (
validate_package_location,
validate_pickup_address_type,
resolve_notification_emails,
build_notification_email_details,
)


def parse_pickup_update_response(
Expand Down Expand Up @@ -46,7 +52,9 @@ def pickup_update_request(
payload: models.PickupUpdateRequest,
settings: provider_utils.Settings,
) -> lib.Serializable:
package_location = validate_package_location(payload.package_location)
address = lib.to_address(payload.address)
notification_emails = resolve_notification_emails(address.email, payload.options)
packages = lib.to_packages(payload.parcels)
options = lib.units.Options(
payload.options,
Expand All @@ -68,6 +76,9 @@ def pickup_update_request(
# fmt: on
),
)
pickup_address_type = validate_pickup_address_type(
options.fedex_pickup_address_type.state
)

# Normalize times to HH:MM format to handle both HH:MM and HH:MM:SS inputs
ready_time = lib.ftime(payload.ready_time, try_formats=["%H:%M:%S", "%H:%M"]) or payload.ready_time
Expand All @@ -79,7 +90,7 @@ def pickup_update_request(
value=settings.account_number,
),
originDetail=fedex.OriginDetailType(
pickupAddressType=options.fedex_pickup_address_type.state,
pickupAddressType=pickup_address_type,
pickupLocation=fedex.PickupLocationType(
contact=fedex.ContactType(
companyName=address.company_name,
Expand All @@ -105,7 +116,7 @@ def pickup_update_request(
readyDateTimestamp=f"{payload.pickup_date}T{ready_time}:00Z",
customerCloseTime=f"{closing_time}:00",
pickupDateType=options.fedex_pickup_date_type.state,
packageLocation=payload.package_location,
packageLocation=package_location,
buildingPart=options.fedex_building_part.state,
buildingPartDescription=options.fedex_building_part_description.state,
earlyPickup=options.fedex_early_pickup.state,
Expand All @@ -129,16 +140,11 @@ def pickup_update_request(
oversizePackageCount=None,
pickupNotificationDetail=lib.identity(
fedex.PickupNotificationDetailType(
emailDetails=[
fedex.EmailDetailType(
address=address.email,
locale="en_US",
)
],
emailDetails=build_notification_email_details(notification_emails),
format="TEXT",
userMessage=options.fedex_user_message.state,
)
if address.email
if any(notification_emails)
else None
),
)
Expand Down
105 changes: 105 additions & 0 deletions modules/connectors/fedex/karrio/providers/fedex/pickup/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import typing
import karrio.lib as lib
import karrio.schemas.fedex.pickup_request as fedex


FEDEX_PACKAGE_LOCATION_VALUES = {"FRONT", "NONE", "REAR", "SIDE"}
FEDEX_PICKUP_ADDRESS_TYPE_VALUES = {"ACCOUNT", "SHIPPER", "OTHER"}
FEDEX_MAX_NOTIFICATION_EMAILS = 5


def _normalize_email_values(
value: typing.Any,
) -> typing.List[str]:
if value is None:
return []

if isinstance(value, (list, tuple, set)):
return [str(item).strip() for item in value if str(item).strip()]

return [
_.strip()
for _ in str(value).replace(";", ",").split(",")
if _.strip()
]


def validate_package_location(value: typing.Optional[str]) -> typing.Optional[str]:
if value is None:
return None

package_location = str(value).strip().upper()
if package_location not in FEDEX_PACKAGE_LOCATION_VALUES:
raise lib.exceptions.FieldError(
{
"package_location": (
f"Invalid FedEx package location '{value}'. "
f"Expected one of: {sorted(FEDEX_PACKAGE_LOCATION_VALUES)}"
)
}
)

return package_location


def validate_pickup_address_type(value: typing.Optional[str]) -> str:
if value is None:
return "OTHER"

pickup_address_type = str(value).strip().upper()
if pickup_address_type not in FEDEX_PICKUP_ADDRESS_TYPE_VALUES:
raise lib.exceptions.FieldError(
{
"fedex_pickup_address_type": (
f"Invalid FedEx pickup address type '{value}'. "
f"Expected one of: {sorted(FEDEX_PICKUP_ADDRESS_TYPE_VALUES)}"
)
}
)

return pickup_address_type


def resolve_notification_emails(
primary_email: typing.Optional[str], options: typing.Optional[dict]
) -> typing.List[str]:
options = options or {}
emails: typing.List[str] = []

if primary_email:
emails.append(str(primary_email).strip())

emails.extend(_normalize_email_values(options.get("email_notification_to")))
emails.extend(_normalize_email_values(options.get("fedex_notification_emails")))

unique_emails: typing.List[str] = []
seen = set()
for email in emails:
key = email.lower()
if key not in seen:
unique_emails.append(email)
seen.add(key)

if len(unique_emails) > FEDEX_MAX_NOTIFICATION_EMAILS:
raise lib.exceptions.FieldError(
{
"fedex_notification_emails": (
f"FedEx pickup notification supports up to {FEDEX_MAX_NOTIFICATION_EMAILS} email addresses. "
f"Received: {len(unique_emails)}"
)
}
)

return unique_emails


def build_notification_email_details(
emails: typing.List[str],
) -> typing.List[fedex.EmailDetailType]:
return [
fedex.EmailDetailType(
address=email,
locale="en_US",
)
for email in emails
]
Loading