From ae498a06eca576f3c85e0e0be41bca32c51b94e6 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Wed, 13 Aug 2025 01:54:11 -0400 Subject: [PATCH 01/23] Add Freightcom Rest shipping integration plugin This commit introduces a new Karrio plugin for Freightcom Rest API, including mappers, schemas, and provider implementations for rate, shipment, and cancellation functionalities. It also adds support for Freightcom-specific settings, utilities, and unit mappings to enable seamless integration and plugin registration. --- plugins/freightcom_rest/README.md | 30 + plugins/freightcom_rest/generate | 13 + .../mappers/freightcom_rest/__init__.py | 3 + .../karrio/mappers/freightcom_rest/mapper.py | 54 + .../karrio/mappers/freightcom_rest/proxy.py | 124 + .../mappers/freightcom_rest/settings.py | 20 + .../plugins/freightcom_rest/__init__.py | 29 + .../providers/freightcom_rest/__init__.py | 16 + .../karrio/providers/freightcom_rest/error.py | 31 + .../providers/freightcom_rest/metadata.json | 4671 +++++++++++++++++ .../karrio/providers/freightcom_rest/rate.py | 252 + .../freightcom_rest/shipment/__init__.py | 9 + .../freightcom_rest/shipment/cancel.py | 75 + .../freightcom_rest/shipment/create.py | 387 ++ .../karrio/providers/freightcom_rest/units.py | 177 + .../karrio/providers/freightcom_rest/utils.py | 107 + .../schemas/freightcom_rest/__init__.py | 0 .../schemas/freightcom_rest/error_response.py | 14 + .../schemas/freightcom_rest/pickup_request.py | 46 + .../schemas/freightcom_rest/rate_request.py | 173 + .../schemas/freightcom_rest/rate_response.py | 49 + .../freightcom_rest/shipment_request.py | 246 + .../freightcom_rest/shipment_response.py | 224 + .../freightcom_rest/tracking_response.py | 23 + plugins/freightcom_rest/pyproject.toml | 39 + .../schemas/error_response.json | 4 + .../schemas/pickup_request.json | 39 + .../freightcom_rest/schemas/rate_request.json | 171 + .../schemas/rate_response.json | 47 + .../schemas/shipment_request.json | 267 + .../schemas/shipment_response.json | 228 + .../schemas/tracking_response.json | 14 + plugins/freightcom_rest/tests/__init__.py | 4 + .../tests/freightcom_rest/__init__.py | 1 + .../tests/freightcom_rest/fixture.py | 22 + .../tests/freightcom_rest/test_rate.py | 458 ++ .../tests/freightcom_rest/test_shipment.py | 487 ++ 37 files changed, 8554 insertions(+) create mode 100644 plugins/freightcom_rest/README.md create mode 100644 plugins/freightcom_rest/generate create mode 100644 plugins/freightcom_rest/karrio/mappers/freightcom_rest/__init__.py create mode 100644 plugins/freightcom_rest/karrio/mappers/freightcom_rest/mapper.py create mode 100644 plugins/freightcom_rest/karrio/mappers/freightcom_rest/proxy.py create mode 100644 plugins/freightcom_rest/karrio/mappers/freightcom_rest/settings.py create mode 100644 plugins/freightcom_rest/karrio/plugins/freightcom_rest/__init__.py create mode 100644 plugins/freightcom_rest/karrio/providers/freightcom_rest/__init__.py create mode 100644 plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py create mode 100644 plugins/freightcom_rest/karrio/providers/freightcom_rest/metadata.json create mode 100644 plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py create mode 100644 plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/__init__.py create mode 100644 plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/cancel.py create mode 100644 plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py create mode 100644 plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py create mode 100644 plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py create mode 100644 plugins/freightcom_rest/karrio/schemas/freightcom_rest/__init__.py create mode 100644 plugins/freightcom_rest/karrio/schemas/freightcom_rest/error_response.py create mode 100644 plugins/freightcom_rest/karrio/schemas/freightcom_rest/pickup_request.py create mode 100644 plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_request.py create mode 100644 plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_response.py create mode 100644 plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_request.py create mode 100644 plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_response.py create mode 100644 plugins/freightcom_rest/karrio/schemas/freightcom_rest/tracking_response.py create mode 100644 plugins/freightcom_rest/pyproject.toml create mode 100644 plugins/freightcom_rest/schemas/error_response.json create mode 100644 plugins/freightcom_rest/schemas/pickup_request.json create mode 100644 plugins/freightcom_rest/schemas/rate_request.json create mode 100644 plugins/freightcom_rest/schemas/rate_response.json create mode 100644 plugins/freightcom_rest/schemas/shipment_request.json create mode 100644 plugins/freightcom_rest/schemas/shipment_response.json create mode 100644 plugins/freightcom_rest/schemas/tracking_response.json create mode 100644 plugins/freightcom_rest/tests/__init__.py create mode 100644 plugins/freightcom_rest/tests/freightcom_rest/__init__.py create mode 100644 plugins/freightcom_rest/tests/freightcom_rest/fixture.py create mode 100644 plugins/freightcom_rest/tests/freightcom_rest/test_rate.py create mode 100644 plugins/freightcom_rest/tests/freightcom_rest/test_shipment.py diff --git a/plugins/freightcom_rest/README.md b/plugins/freightcom_rest/README.md new file mode 100644 index 0000000..035400a --- /dev/null +++ b/plugins/freightcom_rest/README.md @@ -0,0 +1,30 @@ +# karrio.freightcom_rest + +This package is a Freightcom Rest extension of the [karrio](https://pypi.org/project/karrio) multi carrier shipping SDK. + +## Requirements + +`Python 3.11+` + +## Installation + +```bash +pip install karrio.freightcom_rest +``` + +## Usage + +```python +import karrio.sdk as karrio +from karrio.mappers.freightcom_rest.settings import Settings + + +# Initialize a carrier gateway +freightcom_rest = karrio.gateway["freightcom_rest"].create( + Settings( + ... + ) +) +``` + +Check the [Karrio Mutli-carrier SDK docs](https://docs.karrio.io) for Shipping API requests diff --git a/plugins/freightcom_rest/generate b/plugins/freightcom_rest/generate new file mode 100644 index 0000000..66e5b09 --- /dev/null +++ b/plugins/freightcom_rest/generate @@ -0,0 +1,13 @@ +SCHEMAS=./schemas +LIB_MODULES=./karrio/schemas/freightcom_rest +find "${LIB_MODULES}" -name "*.py" -exec rm -r {} \; +touch "${LIB_MODULES}/__init__.py" + +kcli codegen generate "${SCHEMAS}/error_response.json" "${LIB_MODULES}/error_response.py" --nice-property-names +kcli codegen generate "${SCHEMAS}/rate_request.json" "${LIB_MODULES}/rate_request.py" --nice-property-names +kcli codegen generate "${SCHEMAS}/rate_response.json" "${LIB_MODULES}/rate_response.py" --nice-property-names +kcli codegen generate "${SCHEMAS}/shipment_request.json" "${LIB_MODULES}/shipment_request.py" --nice-property-names +kcli codegen generate "${SCHEMAS}/shipment_response.json" "${LIB_MODULES}/shipment_response.py" --nice-property-names +kcli codegen generate "${SCHEMAS}/pickup_request.json" "${LIB_MODULES}/pickup_request.py" --nice-property-names +kcli codegen generate "${SCHEMAS}/tracking_response.json" "${LIB_MODULES}/tracking_response.py" --nice-property-names + diff --git a/plugins/freightcom_rest/karrio/mappers/freightcom_rest/__init__.py b/plugins/freightcom_rest/karrio/mappers/freightcom_rest/__init__.py new file mode 100644 index 0000000..d1473d6 --- /dev/null +++ b/plugins/freightcom_rest/karrio/mappers/freightcom_rest/__init__.py @@ -0,0 +1,3 @@ +from karrio.mappers.freightcom_rest.mapper import Mapper +from karrio.mappers.freightcom_rest.proxy import Proxy +from karrio.mappers.freightcom_rest.settings import Settings \ No newline at end of file diff --git a/plugins/freightcom_rest/karrio/mappers/freightcom_rest/mapper.py b/plugins/freightcom_rest/karrio/mappers/freightcom_rest/mapper.py new file mode 100644 index 0000000..d6043c3 --- /dev/null +++ b/plugins/freightcom_rest/karrio/mappers/freightcom_rest/mapper.py @@ -0,0 +1,54 @@ +"""Karrio Freightcom Rest client mapper.""" + +import typing +import karrio.lib as lib +import karrio.api.mapper as mapper +import karrio.core.models as models +import karrio.providers.freightcom_rest as provider +import karrio.mappers.freightcom_rest.settings as provider_settings + + +class Mapper(mapper.Mapper): + settings: provider_settings.Settings + + def create_rate_request( + self, payload: models.RateRequest + ) -> lib.Serializable: + return provider.rate_request(payload, self.settings) + + # def create_tracking_request( + # self, payload: models.TrackingRequest + # ) -> lib.Serializable: + # return provider.tracking_request(payload, self.settings) + + def create_shipment_request( + self, payload: models.ShipmentRequest + ) -> lib.Serializable: + return provider.shipment_request(payload, self.settings) + + def create_cancel_shipment_request( + self, payload: models.ShipmentCancelRequest + ) -> lib.Serializable[str]: + return provider.shipment_cancel_request(payload, self.settings) + + + def parse_cancel_shipment_response( + self, response: lib.Deserializable[str] + ) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]: + return provider.parse_shipment_cancel_response(response, self.settings) + + def parse_rate_response( + self, response: lib.Deserializable[str] + ) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]: + return provider.parse_rate_response(response, self.settings) + + def parse_shipment_response( + self, response: lib.Deserializable[str] + ) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]: + return provider.parse_shipment_response(response, self.settings) + + # def parse_tracking_response( + # self, response: lib.Deserializable[str] + # ) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]: + # return provider.parse_tracking_response(response, self.settings) + # diff --git a/plugins/freightcom_rest/karrio/mappers/freightcom_rest/proxy.py b/plugins/freightcom_rest/karrio/mappers/freightcom_rest/proxy.py new file mode 100644 index 0000000..672f46a --- /dev/null +++ b/plugins/freightcom_rest/karrio/mappers/freightcom_rest/proxy.py @@ -0,0 +1,124 @@ +"""Karrio Freightcom Rest client proxy.""" + +import time +import karrio.lib as lib +import karrio.api.proxy as proxy +import karrio.mappers.freightcom_rest.settings as provider_settings + +# IMPLEMENTATION INSTRUCTIONS: +# 1. Import the schema types specific to your carrier API +# 2. Uncomment and adapt the request examples below to work with your carrier API +# 3. Replace the stub responses with actual API calls once you've tested with the example data +# 4. Update URLs, headers, and authentication methods as required by your carrier API +MAX_RETRIES = 10 +POLL_INTERVAL = 2 # seconds + + +class Proxy(proxy.Proxy): + settings: provider_settings.Settings + + def get_rates(self, request: lib.Serializable) -> lib.Deserializable: + # Step 1: Submit rate request and get quote ID + response = self._send_request( + path="/rate", request=lib.Serializable(request.value, lib.to_json) + ) + + rate_id = lib.to_dict(response).get('request_id') + if not rate_id: + return lib.Deserializable(response, lib.to_dict) + + # Step 2: Poll for rate results + for _ in range(MAX_RETRIES): + status_res = self._send_request( + path=f"/rate/{rate_id}", + method="GET" + ) + + status = lib.to_dict(status_res).get('status', {}).get('done', False) + + if status: # Quote is complete + return lib.Deserializable(status_res, lib.to_dict) + + time.sleep(POLL_INTERVAL) + + # If we exceed max retries + return lib.Deserializable({ + 'message': 'Rate calculation timed out' + }, lib.to_dict) + + def create_shipment(self, request: lib.Serializable) -> lib.Deserializable: + + response = self._send_request( + path="/shipment", request=lib.Serializable(request.value, lib.to_json) + ) + + shipment_id = lib.to_dict(response).get('id') + if not shipment_id: + return lib.Deserializable(response, lib.to_dict) + + + # Step 2: retry because api return empty bytes if done to fast + time.sleep(1) + for _ in range(MAX_RETRIES): + + shipment_response = self._send_request(path=f"/shipment/{shipment_id}", method="GET") + shipment_res = lib.failsafe(lambda :lib.to_dict(shipment_response)) or lib.decode(shipment_response) + + if shipment_res: # is complete + return lib.Deserializable(shipment_res, lib.to_dict, request._ctx) + + time.sleep(POLL_INTERVAL) + + # If we exceed max retries + return lib.Deserializable({ + 'message': 'timed out creating shipment, shipment maybe created' + }, lib.to_dict) + + + # def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[str]: + # responses = lib.run_asynchronously( + # lambda data: ( + # data["shipment_id"], + # self._send_request(path=f"/shipment/{data['shipment_id']}/tracking-events"), + # ), + # [_ for _ in request.serialize() if _.get("shipment_id")], + # ) + # + # print(lib.to_dict(responses)) + # return lib.Deserializable( + # responses, + # lambda __: [(_[0], lib.to_dict(_[1])) for _ in __], + # ) + + + + def _get_payments_methods(self) -> lib.Deserializable[str]: + response = self._send_request( + path="/finance/payment-methods", + method="GET" + ) + return lib.Deserializable(response, lib.to_dict) + + def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable: + response = self._send_request( + path=f"/shipment/{request.serialize()}", method="DELETE" + ) + return lib.Deserializable(response if any(response) else "{}", lib.to_dict) + + def _send_request( + self, path: str, request: lib.Serializable = None, method: str = "POST" + ) -> str: + + data: dict = dict(data=request.serialize()) if request is not None else dict() + return lib.request( + **{ + "url": f"{self.settings.server_url}{path}", + "trace": self.trace_as("json"), + "method": method, + "headers": { + "Content-Type": "application/json", + "Authorization": self.settings.api_key, + }, + **data, + } + ) diff --git a/plugins/freightcom_rest/karrio/mappers/freightcom_rest/settings.py b/plugins/freightcom_rest/karrio/mappers/freightcom_rest/settings.py new file mode 100644 index 0000000..3514029 --- /dev/null +++ b/plugins/freightcom_rest/karrio/mappers/freightcom_rest/settings.py @@ -0,0 +1,20 @@ +"""Karrio Freightcom Rest client settings.""" + +import attr +import karrio.providers.freightcom_rest.utils as provider_utils + + +@attr.s(auto_attribs=True) +class Settings(provider_utils.Settings): + """Freightcom Rest connection settings.""" + + # Add carrier specific API connection properties here + api_key: str + + # generic properties + id: str = None + test_mode: bool = False + carrier_id: str = "freightcom_rest" + account_country_code: str = None + metadata: dict = {} + config: dict = {} diff --git a/plugins/freightcom_rest/karrio/plugins/freightcom_rest/__init__.py b/plugins/freightcom_rest/karrio/plugins/freightcom_rest/__init__.py new file mode 100644 index 0000000..147073a --- /dev/null +++ b/plugins/freightcom_rest/karrio/plugins/freightcom_rest/__init__.py @@ -0,0 +1,29 @@ +from karrio.core.metadata import PluginMetadata + +from karrio.mappers.freightcom_rest.mapper import Mapper +from karrio.mappers.freightcom_rest.proxy import Proxy +from karrio.mappers.freightcom_rest.settings import Settings +import karrio.providers.freightcom_rest.units as units +import karrio.providers.freightcom_rest.utils as utils + + +# This METADATA object is used by Karrio to discover and register this plugin +# when loaded through Python entrypoints or local plugin directories. +# The entrypoint is defined in pyproject.toml under [project.entry-points."karrio.plugins"] +METADATA = PluginMetadata( + id="freightcom_rest", + label="Freightcom Rest", + description="Freightcom Rest shipping integration for Karrio", + # Integrations + Mapper=Mapper, + Proxy=Proxy, + Settings=Settings, + # Data Units + is_hub=True, + options=units.ShippingOption, + services=units.ShippingService, + connection_configs=utils.ConnectionConfig, + # Extra info + website="https://www.freightcom.com/", + documentation="https://developer.freightcom.com/", +) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/__init__.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/__init__.py new file mode 100644 index 0000000..1a3d868 --- /dev/null +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/__init__.py @@ -0,0 +1,16 @@ +"""Karrio Freightcom Rest provider imports.""" +from karrio.providers.freightcom_rest.utils import Settings +from karrio.providers.freightcom_rest.rate import ( + parse_rate_response, + rate_request, +) +from karrio.providers.freightcom_rest.shipment import ( + parse_shipment_cancel_response, + parse_shipment_response, + shipment_cancel_request, + shipment_request, +) +# from karrio.providers.freightcom_rest.tracking import ( +# parse_tracking_response, +# tracking_request, +# ) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py new file mode 100644 index 0000000..7bff4d3 --- /dev/null +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py @@ -0,0 +1,31 @@ +"""Karrio Freightcom Rest error parser.""" + +import typing +import karrio.lib as lib +import karrio.core.models as models +import karrio.providers.freightcom_rest.utils as provider_utils + + +def parse_error_response( + response: dict, + settings: provider_utils.Settings, + **kwargs, +) -> typing.List[models.Message]: + responses = response if isinstance(response, list) else [response] + + errors = [ + *[_ for _ in responses if _.get("message")], + ] + + return [ + models.Message( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + message=error.get("message"), + details={ + **kwargs, + **(error.get('data', {})) + }, + ) + for error in errors + ] diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/metadata.json b/plugins/freightcom_rest/karrio/providers/freightcom_rest/metadata.json new file mode 100644 index 0000000..861b80d --- /dev/null +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/metadata.json @@ -0,0 +1,4671 @@ +{ + "PROD_SERVICES": [ + { + "id": "dhatttransfreightserviceinc-558.standard", + "carrier_name": "DHATT TRANSFREIGHT SERVICE INC.", + "service_name": "Standard" + }, + { + "id": "kelownaexpressfreightinc-570.standard", + "carrier_name": "Kelowna Express Freight Inc.", + "service_name": "Standard" + }, + { + "id": "metrofreightwaysltd-610.standard", + "carrier_name": "METRO FREIGHTWAYS LTD.", + "service_name": "Standard" + }, + { + "id": "scottfreight-190.standard", + "carrier_name": "Scott Freight", + "service_name": "Standard" + }, + { + "id": "tforcefreight-575.standard", + "carrier_name": "TForce Freight", + "service_name": "Standard" + }, + { + "id": "vkdeliverylinehaulltd-499.standard", + "carrier_name": "VK DELIVERY/LINEHAUL LTD", + "service_name": "Standard" + }, + { + "id": "customcourierco-469.standard", + "carrier_name": "Custom Courier Co.", + "service_name": "Standard" + }, + { + "id": "ecotransinc-396.standard", + "carrier_name": "ECOTRANS INC", + "service_name": "Standard" + }, + { + "id": "fedex.economy", + "carrier_name": "FedEx Freight", + "service_name": "Economy" + }, + { + "id": "fedex.standard", + "carrier_name": "FedEx Freight", + "service_name": "Priority" + }, + { + "id": "gtagsm-540.standard", + "carrier_name": "G.T.A GSM", + "service_name": "Standard" + }, + { + "id": "kindersleytransportusa-551.standard", + "carrier_name": "Kindersley Transport | USA", + "service_name": "Standard" + }, + { + "id": "lodestarlogistics-446.standard", + "carrier_name": "LODESTAR LOGISTICS", + "service_name": "Standard" + }, + { + "id": "loomis-express.express-0900", + "carrier_name": "Loomis", + "service_name": "Express 9:00" + }, + { + "id": "loomis-express.express-1200", + "carrier_name": "Loomis", + "service_name": "Express 12:00" + }, + { + "id": "loomis-express.express-1800", + "carrier_name": "Loomis", + "service_name": "Express 18:00" + }, + { + "id": "loomis-express.ground", + "carrier_name": "Loomis", + "service_name": "Ground" + }, + { + "id": "mrflatbedstransportinc-284.standard", + "carrier_name": "Mr Flatbeds Transport Inc", + "service_name": "Standard" + }, + { + "id": "bisontransport-278.standard", + "carrier_name": "Bison Transport", + "service_name": "Standard" + }, + { + "id": "canpar.ground", + "carrier_name": "Canpar", + "service_name": "Ground" + }, + { + "id": "canpar.international", + "carrier_name": "Canpar", + "service_name": "International" + }, + { + "id": "canpar.overnight", + "carrier_name": "Canpar", + "service_name": "Overnight" + }, + { + "id": "canpar.overnight-letter", + "carrier_name": "Canpar", + "service_name": "Overnight Letter" + }, + { + "id": "canpar.overnight-pak", + "carrier_name": "Canpar", + "service_name": "Overnight Pak" + }, + { + "id": "canpar.select", + "carrier_name": "Canpar", + "service_name": "Select" + }, + { + "id": "canpar.select-letter", + "carrier_name": "Canpar", + "service_name": "Select Letter" + }, + { + "id": "canpar.select-pak", + "carrier_name": "Canpar", + "service_name": "Select Pak" + }, + { + "id": "canpar.select-usa", + "carrier_name": "Canpar", + "service_name": "Select U.S.A." + }, + { + "id": "canpar.usa", + "carrier_name": "Canpar", + "service_name": "U.S.A." + }, + { + "id": "canpar.usa-Letter", + "carrier_name": "Canpar", + "service_name": "U.S.A. Letter" + }, + { + "id": "canpar.usa-pak", + "carrier_name": "Canpar", + "service_name": "U.S.A. Pak" + }, + { + "id": "csa.standard", + "carrier_name": "CSA", + "service_name": "Standard" + }, + { + "id": "ettransportgroup-337.standard", + "carrier_name": "ET Transport Group", + "service_name": "Standard" + }, + { + "id": "guardiumlogisticsltd-608.standard", + "carrier_name": "Guardium Logistics Ltd.", + "service_name": "Standard" + }, + { + "id": "keltictransportation-465.standard", + "carrier_name": "Keltic Transportation", + "service_name": "Standard" + }, + { + "id": "kjlxpressinc-583.standard", + "carrier_name": "KJL Xpress Inc.", + "service_name": "Standard" + }, + { + "id": "newpennmotorexpress-445.standard", + "carrier_name": "New Penn Motor Express", + "service_name": "Standard" + }, + { + "id": "bridgepointlogisticsltd-524.standard", + "carrier_name": "BRIDGEPOINT LOGISTICS LTD", + "service_name": "Standard" + }, + { + "id": "canadapost.domestic", + "carrier_name": "Canada Post", + "service_name": "Domestic" + }, + { + "id": "canadapost.expedited-parcel", + "carrier_name": "Canada Post", + "service_name": "Expedited Parcel" + }, + { + "id": "canadapost.expedited-parcel-usa", + "carrier_name": "Canada Post", + "service_name": "Expedited Parcel USA" + }, + { + "id": "canadapost.international", + "carrier_name": "Canada Post", + "service_name": "International" + }, + { + "id": "canadapost.international-parcel-air", + "carrier_name": "Canada Post", + "service_name": "International Parcel Air" + }, + { + "id": "canadapost.international-parcel-surface", + "carrier_name": "Canada Post", + "service_name": "International Parcel Surface" + }, + { + "id": "canadapost.priority", + "carrier_name": "Canada Post", + "service_name": "Priority" + }, + { + "id": "canadapost.priority-ww-envelope-international", + "carrier_name": "Canada Post", + "service_name": "Priority Worldwide envelope INT’L" + }, + { + "id": "canadapost.priority-ww-envelope-usa", + "carrier_name": "Canada Post", + "service_name": "Priority Worldwide envelope USA" + }, + { + "id": "canadapost.priority-ww-pak-international", + "carrier_name": "Canada Post", + "service_name": "Priority Worldwide pak INT’L" + }, + { + "id": "canadapost.priority-ww-pak-usa", + "carrier_name": "Canada Post", + "service_name": "Priority Worldwide pak USA" + }, + { + "id": "canadapost.priority-ww-parcel-international", + "carrier_name": "Canada Post", + "service_name": "Priority Worldwide parcel INT’L" + }, + { + "id": "canadapost.priority-ww-parcel-usa", + "carrier_name": "Canada Post", + "service_name": "Priority Worldwide parcel USA" + }, + { + "id": "canadapost.regular-parcel", + "carrier_name": "Canada Post", + "service_name": "Regular Parcel" + }, + { + "id": "canadapost.small-packet-international-air", + "carrier_name": "Canada Post", + "service_name": "Small Packet International Air" + }, + { + "id": "canadapost.small-packet-international-surface", + "carrier_name": "Canada Post", + "service_name": "Small Packet International Surface" + }, + { + "id": "canadapost.small-packet-usa-air", + "carrier_name": "Canada Post", + "service_name": "Small Packet USA Air" + }, + { + "id": "canadapost.tracked-packet-international", + "carrier_name": "Canada Post", + "service_name": "Tracked Packet - International" + }, + { + "id": "canadapost.tracked-packet-usa", + "carrier_name": "Canada Post", + "service_name": "Tracked Packet USA" + }, + { + "id": "canadapost.xpresspost", + "carrier_name": "Canada Post", + "service_name": "Xpresspost" + }, + { + "id": "canadapost.xpresspost-international", + "carrier_name": "Canada Post", + "service_name": "Xpresspost International" + }, + { + "id": "canadapost.xpresspost-usa", + "carrier_name": "Canada Post", + "service_name": "Xpresspost USA" + }, + { + "id": "gardewine.standard", + "carrier_name": "Gardewine", + "service_name": "Standard" + }, + { + "id": "giantleaftruckinginc-556.standard", + "carrier_name": "GIANT LEAF TRUCKING INC.", + "service_name": "Standard" + }, + { + "id": "purolatorfreight.standard", + "carrier_name": "Purolator Freight", + "service_name": "Standard" + }, + { + "id": "swyft.nextday", + "carrier_name": "Swyft", + "service_name": "Next Day" + }, + { + "id": "swyft.sameday", + "carrier_name": "Swyft", + "service_name": "Same Day" + }, + { + "id": "usps.ground-advantage", + "carrier_name": "USPS", + "service_name": "Ground Advantage" + }, + { + "id": "usps.priority-mail", + "carrier_name": "USPS", + "service_name": "Priority Mail" + }, + { + "id": "usps.priority-mail-express", + "carrier_name": "USPS", + "service_name": "Priority Mail Express" + }, + { + "id": "xwestcarriersinc-547.standard", + "carrier_name": "X West Carriers Inc", + "service_name": "Standard" + }, + { + "id": "paulsfreightline-400.standard", + "carrier_name": "PAULS FREIGHTLINE", + "service_name": "Standard" + }, + { + "id": "ppgroadlinesinc-250.standard", + "carrier_name": "PPG ROADLINES INC.", + "service_name": "Standard" + }, + { + "id": "transportgilleslavigne-592.standard", + "carrier_name": "Transport Gilles Lavigne", + "service_name": "Standard" + }, + { + "id": "xpologistics-265.standard", + "carrier_name": "XPO Logistics", + "service_name": "Standard" + }, + { + "id": "upscourier-162.standard", + "carrier_name": "UPS Courier", + "service_name": "Standard" + }, + { + "id": "94222528qubecincdbagroupefmj-626.standard", + "carrier_name": "9422-2528 Québec Inc. DBA Groupe FMJ", + "service_name": "Standard" + }, + { + "id": "one.standard", + "carrier_name": "ONE Transportation", + "service_name": "Standard" + }, + { + "id": "readygotransportinc-435.standard", + "carrier_name": "READY GO TRANSPORT INC.", + "service_name": "Standard" + }, + { + "id": "streetkingtransportationltd-557.standard", + "carrier_name": "STREET KING TRANSPORTATION LTD", + "service_name": "Standard" + }, + { + "id": "martinroytransport-310.standard", + "carrier_name": "Martin Roy Transport", + "service_name": "Standard" + }, + { + "id": "sameday.2-man-delivery-to-entrance", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Delivery to Entrance - 2 Person" + }, + { + "id": "sameday.2-man-delivery-to-room-of-choice", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Delivery to Room of Choice - 2 Person" + }, + { + "id": "sameday.2-man-delivery-to-room-of-choice-with-debris-removal", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Two-person delivery to room of choice with debris removal" + }, + { + "id": "sameday.dayr-ecom-urgent-pac", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "eCommerce Urgent Pak" + }, + { + "id": "sameday.delivery-to-entrance", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Delivery to Entrance" + }, + { + "id": "sameday.delivery-to-room-of-choice", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Delivery to Room of Choice" + }, + { + "id": "sameday.delivery-to-room-of-choice-with-debris-removal", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Delivery to Room of Choice with Debris Removal" + }, + { + "id": "sameday.ground-daynross-road", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Ground - Road" + }, + { + "id": "sameday.next-day-before-5pm", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Next Day Delivery by 5 PM" + }, + { + "id": "sameday.next-day-before-9am", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Next Day Delivery by 9 AM" + }, + { + "id": "sameday.next-day-delivery-before-noon", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Next Day Delivery Before Noon" + }, + { + "id": "searcytrucking-462.standard", + "carrier_name": "Searcy Trucking", + "service_name": "Standard" + }, + { + "id": "transkidinc-453.standard", + "carrier_name": "Transkid Inc", + "service_name": "Standard" + }, + { + "id": "amatransinc-251.standard", + "carrier_name": "AMA Trans Inc", + "service_name": "Standard" + }, + { + "id": "checkercourier-276.standard", + "carrier_name": "Checker Courier", + "service_name": "Standard" + }, + { + "id": "empiretransportltd-413.standard", + "carrier_name": "EMPIRE TRANSPORT LTD", + "service_name": "Standard" + }, + { + "id": "flashtransport-388.standard", + "carrier_name": "Flash Transport", + "service_name": "Standard" + }, + { + "id": "wce.standard", + "carrier_name": "WCE", + "service_name": "Intermodal" + }, + { + "id": "xpoglobalforwardinginc-416.standard", + "carrier_name": "XPO GLOBAL FORWARDING INC", + "service_name": "Standard" + }, + { + "id": "kjstransport-623.standard", + "carrier_name": "KJS Transport", + "service_name": "Standard" + }, + { + "id": "ltlexpressfreight-459.standard", + "carrier_name": "LTL EXPRESS FREIGHT", + "service_name": "Standard" + }, + { + "id": "overland.standard", + "carrier_name": "Overland", + "service_name": "Standard" + }, + { + "id": "dbgtrucking-132.standard", + "carrier_name": "DBG TRUCKING ", + "service_name": "Standard" + }, + { + "id": "dhillondhillontransportltd-528.standard", + "carrier_name": "Dhillon & Dhillon Transport Ltd.", + "service_name": "Standard" + }, + { + "id": "freightboy-565.standard", + "carrier_name": "FREIGHT BOY", + "service_name": "Standard" + }, + { + "id": "highrisetransport-545.standard", + "carrier_name": "HIGH RISE TRANSPORT", + "service_name": "Standard" + }, + { + "id": "yrcfreight-330.standard", + "carrier_name": "YRC Freight", + "service_name": "Standard" + }, + { + "id": "caledoncouriers-294.standard", + "carrier_name": "Caledon Couriers", + "service_name": "Standard" + }, + { + "id": "proactivetransportcoproactivesupplycahinsolutionsinc-471.standard", + "carrier_name": "PROACTIVE TRANSPORT C/O PROACTIVE SUPPLYCAHIN SOLUTIONS INC.", + "service_name": "Standard" + }, + { + "id": "roadtrainexpress-433.standard", + "carrier_name": "ROAD TRAIN EXPRESS ", + "service_name": "Standard" + }, + { + "id": "sablemarcoinc-509.standard", + "carrier_name": "Sable Marco inc.", + "service_name": "Standard" + }, + { + "id": "here2therefreightmanagementinc-629.standard", + "carrier_name": "Here 2 There Freight Management Inc", + "service_name": "Standard" + }, + { + "id": "thompsonemergencyfreightsystems-277.standard", + "carrier_name": "Thompson Emergency Freight Systems", + "service_name": "Standard" + }, + { + "id": "dhl-ecomm.packet-international", + "carrier_name": "DHL eCommerce", + "service_name": "Package International" + }, + { + "id": "dhl-ecomm.parcel-expedited", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel Expedited" + }, + { + "id": "dhl-ecomm.parcel-expedited-max", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel Expedited Max" + }, + { + "id": "dhl-ecomm.parcel-ground", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel Ground" + }, + { + "id": "dhl-ecomm.parcel-international-direct", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel International Direct" + }, + { + "id": "dhl-ecomm.parcel-international-direct-priority", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel International Direct Priority" + }, + { + "id": "dhl-ecomm.parcel-international-direct-standard", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel International Direct Standard" + }, + { + "id": "dhl-ecomm.parcel-international-standard", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel International Standard" + }, + { + "id": "excel-transport.standard", + "carrier_name": "Excel Transportation", + "service_name": "Standard" + }, + { + "id": "newpenn.standard", + "carrier_name": "New Penn", + "service_name": "Standard" + }, + { + "id": "bettertrucks.ddu", + "carrier_name": "Better Trucks", + "service_name": "DDU" + }, + { + "id": "bettertrucks.express", + "carrier_name": "Better Trucks", + "service_name": "Express" + }, + { + "id": "bettertrucks.next_day", + "carrier_name": "Better Trucks", + "service_name": "Next Day" + }, + { + "id": "bettertrucks.same_day", + "carrier_name": "Better Trucks", + "service_name": "Same Day" + }, + { + "id": "boxknight.next-day", + "carrier_name": "BoxKnight", + "service_name": "Next Day" + }, + { + "id": "boxknight.sameday", + "carrier_name": "BoxKnight", + "service_name": "Same Day" + }, + { + "id": "cranestransport-555.standard", + "carrier_name": "CRANES TRANSPORT", + "service_name": "Standard" + }, + { + "id": "daynross.cs", + "carrier_name": "Day & Ross", + "service_name": "CS" + }, + { + "id": "daynross.domestic-standard", + "carrier_name": "Day & Ross", + "service_name": "Domestic Standard" + }, + { + "id": "daynross.transborder-standard", + "carrier_name": "Day & Ross", + "service_name": "Transborder Standard" + }, + { + "id": "loadsafecrossborderfreightinc-493.standard", + "carrier_name": "LOADSAFE CROSSBORDER FREIGHT INC.", + "service_name": "Standard" + }, + { + "id": "maritimeontario-267.standard", + "carrier_name": "Maritime-Ontario", + "service_name": "Standard" + }, + { + "id": "saiamotorfreightinc-139.standard", + "carrier_name": "Saia Motor Freight Inc", + "service_name": "Standard" + }, + { + "id": "speedytransport-153.standard", + "carrier_name": "Speedy Transport", + "service_name": "Standard" + }, + { + "id": "bakshbroscartageinc-485.standard", + "carrier_name": "BAKSH BROS. CARTAGE INC", + "service_name": "Standard" + }, + { + "id": "estesexpresslines-274.standard", + "carrier_name": "ESTES Express Lines", + "service_name": "Standard" + }, + { + "id": "highenergytransportinc-533.standard", + "carrier_name": "High Energy Transport Inc", + "service_name": "Standard" + }, + { + "id": "kriskaytrucklinesinc-443.standard", + "carrier_name": "Kris Kay Truck Lines Inc", + "service_name": "Standard" + }, + { + "id": "rollsright-245.standard", + "carrier_name": "Rolls Right", + "service_name": "Standard" + }, + { + "id": "sunshinecoastlogisticsinc-614.standard", + "carrier_name": "Sunshine Coast Logistics Inc", + "service_name": "Standard" + }, + { + "id": "uppaltransportltddbabluewatertrucking-615.standard", + "carrier_name": "Uppal Transport Ltd dba Bluewater Trucking", + "service_name": "Standard" + }, + { + "id": "dayrossfreightrl-529.standard", + "carrier_name": "Day & Ross Freight | R+L", + "service_name": "Standard" + }, + { + "id": "fgmtrucklines-235.standard", + "carrier_name": "FGM Trucklines", + "service_name": "Standard" + }, + { + "id": "kdrtrucklinesinc-627.standard", + "carrier_name": "KDR Trucklines Inc", + "service_name": "Standard" + }, + { + "id": "psrlogisticsinc-506.standard", + "carrier_name": "PSR LOGISTICS INC.", + "service_name": "Standard" + }, + { + "id": "northplusgroupofcompanies-568.standard", + "carrier_name": "NORTH PLUS GROUP OF COMPANIES", + "service_name": "Standard" + }, + { + "id": "swiftdeliverysystems-268.standard", + "carrier_name": "Swift Delivery Systems", + "service_name": "Standard" + }, + { + "id": "vitran.maxx", + "carrier_name": "Vitran", + "service_name": "Maxx" + }, + { + "id": "vitran.priority", + "carrier_name": "Vitran", + "service_name": "Priority" + }, + { + "id": "vitran.regular", + "carrier_name": "Vitran", + "service_name": "Regular" + }, + { + "id": "fastnflowlogistics-487.standard", + "carrier_name": "FAST N FLOW LOGISTICS", + "service_name": "Standard" + }, + { + "id": "fpifreightpartnersinternationalinc-458.standard", + "carrier_name": "FPI - FREIGHT PARTNERS INTERNATIONAL INC.", + "service_name": "Standard" + }, + { + "id": "gsm.air-skip", + "carrier_name": "GTA GSM", + "service_name": "AirSkip" + }, + { + "id": "gsm.air-skip-plus", + "carrier_name": "GTA GSM", + "service_name": "AirSkip+" + }, + { + "id": "gsm.armed-secure-air", + "carrier_name": "GTA GSM", + "service_name": "Armed Secure Air" + }, + { + "id": "gsm.armed-secure-ground", + "carrier_name": "GTA GSM", + "service_name": "Armed Secure Ground" + }, + { + "id": "gsm.ground", + "carrier_name": "GTA GSM", + "service_name": "Ground" + }, + { + "id": "gsm.secure-air", + "carrier_name": "GTA GSM", + "service_name": "Secure Air" + }, + { + "id": "gsm.secure-ground", + "carrier_name": "GTA GSM", + "service_name": "Secure Ground" + }, + { + "id": "gsm.zone-skip", + "carrier_name": "GTA GSM", + "service_name": "ZoneSkip" + }, + { + "id": "gsm.zone-skip-plus", + "carrier_name": "GTA GSM", + "service_name": "ZoneSkip+" + }, + { + "id": "mcarthurexpress-503.standard", + "carrier_name": "Mcarthur Express", + "service_name": "Standard" + }, + { + "id": "himmatpuralogisticsinc-473.standard", + "carrier_name": "HIMMATPURA LOGISTICS INC", + "service_name": "Standard" + }, + { + "id": "proformanceintermodalinc-489.standard", + "carrier_name": "Pro-Formance Intermodal Inc.", + "service_name": "Standard" + }, + { + "id": "whistlercourier-621.standard", + "carrier_name": "Whistler Courier", + "service_name": "Standard" + }, + { + "id": "armourtransportationsystems-562.standard", + "carrier_name": "Armour Transportation Systems", + "service_name": "Standard" + }, + { + "id": "fastfrategroup-498.standard", + "carrier_name": "Fastfrate Group", + "service_name": "Standard" + }, + { + "id": "glotransports-530.standard", + "carrier_name": "GLO TRANSPORTS", + "service_name": "Standard" + }, + { + "id": "gsdirect-292.standard", + "carrier_name": "G&S Direct", + "service_name": "Standard" + }, + { + "id": "gls.ground", + "carrier_name": "GLS", + "service_name": "Ground" + }, + { + "id": "lighthousetransportationinc-619.standard", + "carrier_name": "LIGHTHOUSE TRANSPORTATION INC.", + "service_name": "Standard" + }, + { + "id": "pbtransport-507.standard", + "carrier_name": "P&B TRANSPORT", + "service_name": "Standard" + }, + { + "id": "servicestarfreightways-441.standard", + "carrier_name": "ServiceStar Freightways", + "service_name": "Standard" + }, + { + "id": "tforcefreight.tforcefreight-guarnteed", + "carrier_name": "TForceFreight", + "service_name": "TForceFreight Guaranteed" + }, + { + "id": "tforcefreight.tforcefreight-ltl", + "carrier_name": "TForceFreight", + "service_name": "TForceFreight LTL" + }, + { + "id": "tforcefreight.tforcefreight-standard", + "carrier_name": "TForceFreight", + "service_name": "TForceFreight Standard" + }, + { + "id": "zipcourier-468.standard", + "carrier_name": "Zip Courier", + "service_name": "Standard" + }, + { + "id": "cmwexpress-537.standard", + "carrier_name": "CMW Express", + "service_name": "Standard" + }, + { + "id": "ktslogisticsinc-620.standard", + "carrier_name": "KTS LOGISTICS INC", + "service_name": "Standard" + }, + { + "id": "raymanmotorfreight-357.standard", + "carrier_name": "Rayman Motor Freight", + "service_name": "Standard" + }, + { + "id": "safeloadtransportltd-510.standard", + "carrier_name": "SAFE LOAD TRANSPORT LTD. ", + "service_name": "Standard" + }, + { + "id": "milyardgroupinc-391.standard", + "carrier_name": "Milyard Group Inc.", + "service_name": "Standard" + }, + { + "id": "minimax.standard", + "carrier_name": "Minimax", + "service_name": "Standard" + }, + { + "id": "suretrackgroup-369.standard", + "carrier_name": "SureTrack Group", + "service_name": "Standard" + }, + { + "id": "ctmatransport-535.standard", + "carrier_name": "CTMA Transport", + "service_name": "Standard" + }, + { + "id": "kawarthacourier-425.standard", + "carrier_name": "Kawartha Courier", + "service_name": "Standard" + }, + { + "id": "kepatransportinc-516.standard", + "carrier_name": "KEPA Transport Inc", + "service_name": "Standard" + }, + { + "id": "kindersley-freight.domestic-expedited", + "carrier_name": "Kindersley Transport", + "service_name": "Domestic Expedited" + }, + { + "id": "kindersley-freight.domestic-rail", + "carrier_name": "Kindersley Transport", + "service_name": "Domestic Rail" + }, + { + "id": "kindersley-freight.domestic-road", + "carrier_name": "Kindersley Transport", + "service_name": "Domestic Road" + }, + { + "id": "kindersley-freight.transborder", + "carrier_name": "Kindersley Transport", + "service_name": "Transborder" + }, + { + "id": "boeingtruckinginc-593.standard", + "carrier_name": "Boeing Trucking Inc. ", + "service_name": "Standard" + }, + { + "id": "freightcom-134.standard", + "carrier_name": "Freightcom", + "service_name": "Standard" + }, + { + "id": "groupemorneau-599.standard", + "carrier_name": "Groupe Morneau", + "service_name": "Standard" + }, + { + "id": "xpofreightbrokerage-280.standard", + "carrier_name": "XPO Freight Brokerage", + "service_name": "Standard" + }, + { + "id": "aalfatransportationcorporation-596.standard", + "carrier_name": "A ALFA TRANSPORTATION CORPORATION", + "service_name": "Standard" + }, + { + "id": "barriedirecttransportation-428.standard", + "carrier_name": "Barrie Direct Transportation ", + "service_name": "Standard" + }, + { + "id": "fastfrate.express", + "carrier_name": "Fastfrate", + "service_name": "Express" + }, + { + "id": "fastfrate.standard", + "carrier_name": "Fastfrate", + "service_name": "Standard" + }, + { + "id": "flatouttransportation-566.standard", + "carrier_name": "Flat Out Transportation", + "service_name": "Standard" + }, + { + "id": "reddaway.guaranteed-3pm", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Delivery Before 3:30 PM" + }, + { + "id": "reddaway.guaranteed-9am", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Delivery Before 9 AM" + }, + { + "id": "reddaway.guaranteed-noon", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Delivery Before 12:00 PM (noon)" + }, + { + "id": "reddaway.guaranteed-weekend", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Weekend" + }, + { + "id": "reddaway.guarenteed-9am", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Delivery Before 9 AM" + }, + { + "id": "reddaway.guarenteed-noon", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Delivery Before 12:00 PM (noon)" + }, + { + "id": "reddaway.interline", + "carrier_name": "Reddaway", + "service_name": "Interline Delivery" + }, + { + "id": "reddaway.multi-hour-window", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Window - Multi-Hour Window" + }, + { + "id": "reddaway.regional-delivery", + "carrier_name": "Reddaway", + "service_name": "Regional Delivery" + }, + { + "id": "reddaway.single-hour-window", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Window - Single-hour Window" + }, + { + "id": "reddaway.single-or-multi-day", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Window - Single or Multi-day Window" + }, + { + "id": "reddaway.standard", + "carrier_name": "Reddaway", + "service_name": "Standard" + }, + { + "id": "urbanvalleytransport-526.standard", + "carrier_name": "Urban Valley Transport ", + "service_name": "Standard" + }, + { + "id": "xrpexpress-618.standard", + "carrier_name": "XRP Express", + "service_name": "Standard" + }, + { + "id": "actionforcetransportltd-363.standard", + "carrier_name": "Action Force Transport Ltd", + "service_name": "Standard" + }, + { + "id": "airpro-255.standard", + "carrier_name": "Air Pro", + "service_name": "Standard" + }, + { + "id": "asslafreightinc-588.standard", + "carrier_name": "ASSLA FREIGHT INC.", + "service_name": "Standard" + }, + { + "id": "dhlexpress.domestic-express", + "carrier_name": "DHL Express", + "service_name": "Domestic Express" + }, + { + "id": "dhlexpress.domestic-express1030am", + "carrier_name": "DHL Express", + "service_name": "Domestic Express 10:30" + }, + { + "id": "dhlexpress.domestic-express9am", + "carrier_name": "DHL Express", + "service_name": "Domestic Express 9:00" + }, + { + "id": "dhlexpress.economy-select", + "carrier_name": "DHL Express", + "service_name": "Economy Select" + }, + { + "id": "dhlexpress.express-easy", + "carrier_name": "DHL Express", + "service_name": "Express Easy" + }, + { + "id": "dhlexpress.express-worldwide", + "carrier_name": "DHL Express", + "service_name": "Express Worldwide" + }, + { + "id": "dhlexpress.express1030am", + "carrier_name": "DHL Express", + "service_name": "Express 10:30" + }, + { + "id": "dhlexpress.express12pm", + "carrier_name": "DHL Express", + "service_name": "Express 12:00" + }, + { + "id": "dhlexpress.express9am", + "carrier_name": "DHL Express", + "service_name": "Express 9:00" + }, + { + "id": "intelcom.standard", + "carrier_name": "Intelcom", + "service_name": "Standard" + }, + { + "id": "jardinetransport-217.standard", + "carrier_name": "JARDINE TRANSPORT", + "service_name": "Standard" + }, + { + "id": "sutcocontractingltddbasutcotransportationspecialist-552.standard", + "carrier_name": "SUTCO CONTRACTING LTD./DBA SUTCO TRANSPORTATION SPECIALIST", + "service_name": "Standard" + }, + { + "id": "whistler99courier-621.standard", + "carrier_name": "Whistler 99 Courier", + "service_name": "Standard" + }, + { + "id": "apps.intermodal", + "carrier_name": "APPS", + "service_name": "Intermodal" + }, + { + "id": "apps.standard", + "carrier_name": "APPS", + "service_name": "Standard" + }, + { + "id": "dbgtrucking-527.standard", + "carrier_name": "DBG TRUCKING ", + "service_name": "Standard" + }, + { + "id": "deliverytechinc-442.standard", + "carrier_name": "Delivery Tech Inc", + "service_name": "Standard" + }, + { + "id": "hiway.standard", + "carrier_name": "Hi-Way9", + "service_name": "Standard" + }, + { + "id": "speedxtransport-319.standard", + "carrier_name": "SpeedX Transport", + "service_name": "Standard" + }, + { + "id": "alldaystransportltd-449.standard", + "carrier_name": "ALL DAYS TRANSPORT LTD", + "service_name": "Standard" + }, + { + "id": "dhlexpress-230.standard", + "carrier_name": "DHL Express", + "service_name": "Standard" + }, + { + "id": "freightcomfulfilment-429.standard", + "carrier_name": "Freightcom Fulfilment", + "service_name": "Standard" + }, + { + "id": "makfreightlinesltd-422.standard", + "carrier_name": "MAK FREIGHT LINES LTD", + "service_name": "Standard" + }, + { + "id": "nationex.standard", + "carrier_name": "Nationex", + "service_name": "Standard" + }, + { + "id": "pacemarathon-611.standard", + "carrier_name": "Pace Marathon", + "service_name": "Standard" + }, + { + "id": "transprofreightsystemsltd-394.standard", + "carrier_name": "Transpro Freight Systems Ltd", + "service_name": "Standard" + }, + { + "id": "diamonddelivery-275.standard", + "carrier_name": "Diamond Delivery", + "service_name": "Standard" + }, + { + "id": "driverdirect-586.standard", + "carrier_name": "Driver Direct", + "service_name": "Standard" + }, + { + "id": "inetexpress-521.standard", + "carrier_name": "I-Net Express", + "service_name": "Standard" + }, + { + "id": "morneau.standard", + "carrier_name": "Morneau Transport", + "service_name": "Standard" + }, + { + "id": "wtmlogisticsltd-579.standard", + "carrier_name": "WTM LOGISTICS LTD.", + "service_name": "Standard" + }, + { + "id": "ab-courier.canada-1030am", + "carrier_name": "A&B Courier", + "service_name": "Canada 10:30am" + }, + { + "id": "ab-courier.canada-930am", + "carrier_name": "A&B Courier", + "service_name": "Canada 9:30am" + }, + { + "id": "ab-courier.canada-ground", + "carrier_name": "A&B Courier", + "service_name": "Canada Ground" + }, + { + "id": "ab-courier.canada-overnight", + "carrier_name": "A&B Courier", + "service_name": "Canada Overnight" + }, + { + "id": "ab-courier.direct", + "carrier_name": "A&B Courier", + "service_name": "Direct" + }, + { + "id": "ab-courier.four-hour", + "carrier_name": "A&B Courier", + "service_name": "Four Hour" + }, + { + "id": "ab-courier.rush", + "carrier_name": "A&B Courier", + "service_name": "Rush" + }, + { + "id": "ab-courier.sameday", + "carrier_name": "A&B Courier", + "service_name": "Sameday" + }, + { + "id": "ab-courier.usa-ground", + "carrier_name": "A&B Courier", + "service_name": "USA Ground" + }, + { + "id": "daytonfreight-581.standard", + "carrier_name": "Dayton Freight", + "service_name": "Standard" + }, + { + "id": "mtslogisticsint-307.standard", + "carrier_name": "MTS Logistics Int.", + "service_name": "Standard" + }, + { + "id": "quikxtransportationinc-490.standard", + "carrier_name": "Quik X Transportation Inc.", + "service_name": "Standard" + }, + { + "id": "sabbystransportinc-574.standard", + "carrier_name": "Sabby?s Transport Inc ", + "service_name": "Standard" + }, + { + "id": "spring-gds.spring-direct", + "carrier_name": "Spring GDS", + "service_name": "Spring Direct" + }, + { + "id": "spring-gds.spring-gateway-parcel", + "carrier_name": "Spring GDS", + "service_name": "Spring Gateway Parcel" + }, + { + "id": "spring-gds.spring-packet-plus", + "carrier_name": "Spring GDS", + "service_name": "Spring Packet Plus Registered" + }, + { + "id": "spring-gds.spring-packet-tracked", + "carrier_name": "Spring GDS", + "service_name": "Spring Packet Tracked" + }, + { + "id": "spring-gds.spring-packet-untracked", + "carrier_name": "Spring GDS", + "service_name": "Spring Packet Untracked" + }, + { + "id": "lplogisticsinc-622.standard", + "carrier_name": "LP Logistics Inc", + "service_name": "Standard" + }, + { + "id": "pctransport-256.standard", + "carrier_name": "PC Transport", + "service_name": "Standard" + }, + { + "id": "peaktransport-497.standard", + "carrier_name": "PEAK TRANSPORT", + "service_name": "Standard" + }, + { + "id": "polaristransportation-188.standard", + "carrier_name": "Polaris Transportation ", + "service_name": "Standard" + }, + { + "id": "uts-470.standard", + "carrier_name": "UTS ", + "service_name": "Standard" + }, + { + "id": "dhillonsnationalservices-531.standard", + "carrier_name": "DHILLON'S NATIONAL SERVICES", + "service_name": "Standard" + }, + { + "id": "gryphontransportationinc-563.standard", + "carrier_name": "GRYPHON TRANSPORTATION INC.", + "service_name": "Standard" + }, + { + "id": "holmesfreight-346.standard", + "carrier_name": "Holmes Freight", + "service_name": "Standard" + }, + { + "id": "transporttransramexpressinc-370.standard", + "carrier_name": "Transport Transram Express Inc", + "service_name": "Standard" + }, + { + "id": "overlandwestfreightlines-253.standard", + "carrier_name": "Overland West Freight Lines", + "service_name": "Standard" + }, + { + "id": "cbstealthexpressinc-520.standard", + "carrier_name": "C.B. Stealth Express Inc.", + "service_name": "Standard" + }, + { + "id": "geowavelogistics-604.standard", + "carrier_name": "Geo Wave Logistics", + "service_name": "Standard" + }, + { + "id": "heartlandtransportltd-624.standard", + "carrier_name": "Heartland Transport Ltd.", + "service_name": "Standard" + }, + { + "id": "kindersley-courier.standard", + "carrier_name": "Kindersley Transport", + "service_name": "Standard" + }, + { + "id": "diamondtransportlogistics-477.standard", + "carrier_name": "Diamond Transport Logistics", + "service_name": "Standard" + }, + { + "id": "maritime.dry", + "carrier_name": "Maritime", + "service_name": "Dry" + }, + { + "id": "maritime.frozen", + "carrier_name": "Maritime", + "service_name": "Reefer" + }, + { + "id": "maritime.heat", + "carrier_name": "Maritime", + "service_name": "Heat" + }, + { + "id": "proactiftransportinc-460.standard", + "carrier_name": "ProActif Transport Inc.", + "service_name": "Standard" + }, + { + "id": "rightservicerightchoicetransportationandwarehouse-578.standard", + "carrier_name": "RIGHT SERVICE RIGHT CHOICE TRANSPORTATION AND WAREHOUSE", + "service_name": "Standard" + }, + { + "id": "averittexpress-576.standard", + "carrier_name": "Averitt Express", + "service_name": "Standard" + }, + { + "id": "bitzertruckingltd-591.standard", + "carrier_name": "Bitzer Trucking Ltd", + "service_name": "Standard" + }, + { + "id": "cctcanada-211.standard", + "carrier_name": "CCT Canada", + "service_name": "Standard" + }, + { + "id": "centraltransport-427.standard", + "carrier_name": "Central Transport", + "service_name": "Standard" + }, + { + "id": "saraixpresstruckinginc-336.standard", + "carrier_name": "SARAI XPRESS TRUCKING INC", + "service_name": "Standard" + }, + { + "id": "onroutefreightservicesinc-598.standard", + "carrier_name": "ONROUTE FREIGHT SERVICES INC.", + "service_name": "Standard" + }, + { + "id": "ontargettransportation-283.standard", + "carrier_name": "On Target Transportation", + "service_name": "Standard" + }, + { + "id": "sunstarhaulersinc-590.standard", + "carrier_name": "SUNSTAR HAULERS INC.", + "service_name": "Standard" + }, + { + "id": "transportecononord-495.standard", + "carrier_name": "Transport Econo Nord", + "service_name": "Standard" + }, + { + "id": "allspeed-432.standard", + "carrier_name": "All Speed", + "service_name": "Standard" + }, + { + "id": "canedatransport-189.standard", + "carrier_name": "Caneda Transport", + "service_name": "Standard" + }, + { + "id": "globaltranslogisticsltd-464.standard", + "carrier_name": "GLOBAL TRANS & LOGISTICS LTD", + "service_name": "Standard" + }, + { + "id": "holland.guaranteed-330-pm", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed by 3:30 PM" + }, + { + "id": "holland.guaranteed-9-am", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed by 9:00 AM" + }, + { + "id": "holland.guaranteed-day", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed Day" + }, + { + "id": "holland.guaranteed-hour", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed Hour" + }, + { + "id": "holland.guaranteed-multi-hour", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed Multi Hour" + }, + { + "id": "holland.guaranteed-noon", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed by Noon" + }, + { + "id": "holland.guaranteed-weekend", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed Weekend" + }, + { + "id": "holland.inter-regional", + "carrier_name": "Holland Freight", + "service_name": "Inter-Regional" + }, + { + "id": "holland.interline", + "carrier_name": "Holland Freight", + "service_name": "Interline" + }, + { + "id": "holland.regional", + "carrier_name": "Holland Freight", + "service_name": "Regional" + }, + { + "id": "westerncanadaexpress-259.standard", + "carrier_name": "Western Canada Express", + "service_name": "Standard" + }, + { + "id": "roadlinkxpress-553.standard", + "carrier_name": "ROAD LINK XPRESS", + "service_name": "Standard" + }, + { + "id": "stishehwaztransportinc-597.standard", + "carrier_name": "STI - Shehwaz Transport Inc.", + "service_name": "Standard" + }, + { + "id": "jbfexpress-430.standard", + "carrier_name": "JBF Express", + "service_name": "Standard" + }, + { + "id": "mjexpress-377.standard", + "carrier_name": "MJ Express", + "service_name": "Standard" + }, + { + "id": "pdfreight-612.standard", + "carrier_name": "PD Freight", + "service_name": "Standard" + }, + { + "id": "riserstransportinc-523.standard", + "carrier_name": "RISERS TRANSPORT INC.", + "service_name": "Standard" + }, + { + "id": "greenwaycarriers-475.standard", + "carrier_name": "GREENWAY CARRIERS ", + "service_name": "Standard" + }, + { + "id": "kimberlytransportltd-327.standard", + "carrier_name": "Kimberly Transport Ltd", + "service_name": "Standard" + }, + { + "id": "kindersleytransport-263.standard", + "carrier_name": "Kindersley Transport", + "service_name": "Standard" + }, + { + "id": "southeasternfreightlines-573.standard", + "carrier_name": "Southeastern Freight Lines", + "service_name": "Standard" + }, + { + "id": "dayton-freight.standard", + "carrier_name": "Dayton Freight", + "service_name": "Standard" + }, + { + "id": "otxlogisticscanadaltd-414.standard", + "carrier_name": "OTX Logistics Canada Ltd", + "service_name": "Standard" + }, + { + "id": "overlandfreightinternational-534.standard", + "carrier_name": "Overland Freight International", + "service_name": "Standard" + }, + { + "id": "westtransautoinc-367.standard", + "carrier_name": "WestTransAuto Inc.", + "service_name": "Standard" + }, + { + "id": "hollandmotorfreight-371.standard", + "carrier_name": "Holland Motor Freight", + "service_name": "Standard" + }, + { + "id": "lowfreightratecaltd-406.standard", + "carrier_name": "LOW FREIGHT RATE.CA LTD", + "service_name": "Standard" + }, + { + "id": "precisiontrucklines-410.standard", + "carrier_name": "PRECISION TRUCK LINES", + "service_name": "Standard" + }, + { + "id": "sewaenterpriseltd-405.standard", + "carrier_name": "SEWA ENTERPRISE LTD", + "service_name": "Standard" + }, + { + "id": "ab-courier-ltl.ltl-direct", + "carrier_name": "A&B Courier", + "service_name": "Direct (Pallet)" + }, + { + "id": "ab-courier-ltl.ltl-rush", + "carrier_name": "A&B Courier", + "service_name": "Rush (Pallet)" + }, + { + "id": "ab-courier-ltl.ltl-sameday", + "carrier_name": "A&B Courier", + "service_name": "Sameday (Pallet)" + }, + { + "id": "bmptransport-318.standard", + "carrier_name": "BMP Transport", + "service_name": "Standard" + }, + { + "id": "courrierplus-494.standard", + "carrier_name": "Courrier Plus", + "service_name": "Standard" + }, + { + "id": "fedexground-426.standard", + "carrier_name": "FedEx Ground", + "service_name": "Standard" + }, + { + "id": "mototransportation-264.standard", + "carrier_name": "Moto Transportation", + "service_name": "Standard" + }, + { + "id": "acecourier-519.standard", + "carrier_name": "ACE Courier", + "service_name": "Standard" + }, + { + "id": "caneda-189.standard", + "carrier_name": "Caneda", + "service_name": "Standard" + }, + { + "id": "expotrans-518.standard", + "carrier_name": "EXPOTRANS", + "service_name": "Standard" + }, + { + "id": "frontlinecarriersystemsinc-496.standard", + "carrier_name": "Frontline Carrier Systems Inc.", + "service_name": "Standard" + }, + { + "id": "frontlinefreight-492.standard", + "carrier_name": "Frontline Freight", + "service_name": "Standard" + }, + { + "id": "nishantransportinc-423.standard", + "carrier_name": "NISHAN TRANSPORT INC.", + "service_name": "Standard" + }, + { + "id": "transamcarriersinc-482.standard", + "carrier_name": "TRANSAM CARRIERS INC", + "service_name": "Standard" + }, + { + "id": "aarontruckingltd-513.standard", + "carrier_name": "AARON TRUCKING LTD", + "service_name": "Standard" + }, + { + "id": "apexmotorexpress-258.standard", + "carrier_name": "Apex Motor Express", + "service_name": "Standard" + }, + { + "id": "comox.standard", + "carrier_name": "Comox Pacific Express", + "service_name": "Standard" + }, + { + "id": "fleet-optics.standard", + "carrier_name": "Fleet Optics", + "service_name": "Standard" + }, + { + "id": "garrymercertrucking-452.standard", + "carrier_name": "Garry Mercer Trucking", + "service_name": "Standard" + }, + { + "id": "hnmlogisticsinc-436.standard", + "carrier_name": "HNM LOGISTICS INC.", + "service_name": "Standard" + }, + { + "id": "smartexpressltd-488.standard", + "carrier_name": "s.m.a.r.t. Express Ltd.", + "service_name": "Standard" + }, + { + "id": "aaacooper-594.standard", + "carrier_name": "AAA Cooper", + "service_name": "Standard" + }, + { + "id": "abcourier-480.standard", + "carrier_name": "A&B Courier", + "service_name": "Standard" + }, + { + "id": "cretransport-287.standard", + "carrier_name": "CRE Transport", + "service_name": "Standard" + }, + { + "id": "fedexexpress-272.standard", + "carrier_name": "FedEx Express", + "service_name": "Standard" + }, + { + "id": "rangefreightwaysltd-448.standard", + "carrier_name": "Range Freightways Ltd.", + "service_name": "Standard" + }, + { + "id": "recalltransportservicesinc-609.standard", + "carrier_name": "Recall Transport Services Inc.", + "service_name": "Standard" + }, + { + "id": "saraixpresstruckinginc-379.standard", + "carrier_name": "Sarai Xpress Trucking Inc", + "service_name": "Standard" + }, + { + "id": "trans2-293.standard", + "carrier_name": "Trans2", + "service_name": "Standard" + }, + { + "id": "coastlineexpress-536.standard", + "carrier_name": "Coastline Express", + "service_name": "Standard" + }, + { + "id": "d4logisticsinc-505.standard", + "carrier_name": "D4 LOGISTICS INC.", + "service_name": "Standard" + }, + { + "id": "exceltransportation-328.standard", + "carrier_name": "Excel Transportation", + "service_name": "Standard" + }, + { + "id": "purolatorcourier.express", + "carrier_name": "Purolator", + "service_name": "Express" + }, + { + "id": "purolatorcourier.express-box", + "carrier_name": "Purolator", + "service_name": "Express Box" + }, + { + "id": "purolatorcourier.express-box-international", + "carrier_name": "Purolator", + "service_name": "Express Box International" + }, + { + "id": "purolatorcourier.express-box1030am", + "carrier_name": "Purolator", + "service_name": "Express Box 10:30 AM" + }, + { + "id": "purolatorcourier.express-box9am", + "carrier_name": "Purolator", + "service_name": "Express Box 9 AM" + }, + { + "id": "purolatorcourier.express-boxUS", + "carrier_name": "Purolator", + "service_name": "Express Box U.S." + }, + { + "id": "purolatorcourier.express-envelope", + "carrier_name": "Purolator", + "service_name": "Express Envelope" + }, + { + "id": "purolatorcourier.express-envelope-international", + "carrier_name": "Purolator", + "service_name": "Express Envelope International" + }, + { + "id": "purolatorcourier.express-envelope-us", + "carrier_name": "Purolator", + "service_name": "Express Envelope U.S." + }, + { + "id": "purolatorcourier.express-envelope1030am", + "carrier_name": "Purolator", + "service_name": "Express Envelope 10:30 AM" + }, + { + "id": "purolatorcourier.express-envelope9am", + "carrier_name": "Purolator", + "service_name": "Express Envelope 9 AM" + }, + { + "id": "purolatorcourier.express-international", + "carrier_name": "Purolator", + "service_name": "Express International" + }, + { + "id": "purolatorcourier.express-pack", + "carrier_name": "Purolator", + "service_name": "Express Pack" + }, + { + "id": "purolatorcourier.express-pack-international", + "carrier_name": "Purolator", + "service_name": "Express Pack International" + }, + { + "id": "purolatorcourier.express-pack-us", + "carrier_name": "Purolator", + "service_name": "Express Pack U.S." + }, + { + "id": "purolatorcourier.express-pack1030am", + "carrier_name": "Purolator", + "service_name": "Express Pack 10:30 AM" + }, + { + "id": "purolatorcourier.express-pack9am", + "carrier_name": "Purolator", + "service_name": "Express Pack 9 AM" + }, + { + "id": "purolatorcourier.express-us", + "carrier_name": "Purolator", + "service_name": "Express U.S." + }, + { + "id": "purolatorcourier.express-us-1030am", + "carrier_name": "Purolator", + "service_name": "Express U.S. 10:30 AM" + }, + { + "id": "purolatorcourier.express-us-9am", + "carrier_name": "Purolator", + "service_name": "Express U.S. 9 AM" + }, + { + "id": "purolatorcourier.express-us-box1030AM", + "carrier_name": "Purolator", + "service_name": "Express U.S. Box 10:30 AM" + }, + { + "id": "purolatorcourier.express-us-box9AM", + "carrier_name": "Purolator", + "service_name": "Express U.S. Box 9 AM" + }, + { + "id": "purolatorcourier.express-us-envelope1030am", + "carrier_name": "Purolator", + "service_name": "Express U.S. Envelope 10:30 AM" + }, + { + "id": "purolatorcourier.express-us-envelope9am", + "carrier_name": "Purolator", + "service_name": "Express U.S. Envelope 9 AM" + }, + { + "id": "purolatorcourier.express-us-pack1030am", + "carrier_name": "Purolator", + "service_name": "Express U.S. Pack 10:30 AM" + }, + { + "id": "purolatorcourier.express-us-pack9am", + "carrier_name": "Purolator", + "service_name": "Express U.S. Pack 9 AM" + }, + { + "id": "purolatorcourier.express1030AM", + "carrier_name": "Purolator", + "service_name": "Express 10:30 AM" + }, + { + "id": "purolatorcourier.express9AM", + "carrier_name": "Purolator", + "service_name": "Express 9 AM" + }, + { + "id": "purolatorcourier.ground", + "carrier_name": "Purolator", + "service_name": "Ground" + }, + { + "id": "purolatorcourier.ground-us", + "carrier_name": "Purolator", + "service_name": "Ground U.S." + }, + { + "id": "purolatorcourier.standard", + "carrier_name": "Purolator", + "service_name": "Standard" + }, + { + "id": "a1delivery-514.standard", + "carrier_name": "A-1 Delivery", + "service_name": "Standard" + }, + { + "id": "fedex-courier.2-day", + "carrier_name": "FedEx Courier", + "service_name": "2-Day" + }, + { + "id": "fedex-courier.2-day-a-m", + "carrier_name": "FedEx Courier", + "service_name": "2-Day AM" + }, + { + "id": "fedex-courier.express-saver", + "carrier_name": "FedEx Courier", + "service_name": "Express Saver" + }, + { + "id": "fedex-courier.first-overnight", + "carrier_name": "FedEx Courier", + "service_name": "First Overnight" + }, + { + "id": "fedex-courier.ground", + "carrier_name": "FedEx Courier", + "service_name": "Ground" + }, + { + "id": "fedex-courier.international-connect-plus", + "carrier_name": "FedEx Courier", + "service_name": "International Connect Plus" + }, + { + "id": "fedex-courier.international-economy", + "carrier_name": "FedEx Courier", + "service_name": "International Economy" + }, + { + "id": "fedex-courier.international-ground", + "carrier_name": "FedEx Courier", + "service_name": "International Ground" + }, + { + "id": "fedex-courier.international-priority", + "carrier_name": "FedEx Courier", + "service_name": "International Priority" + }, + { + "id": "fedex-courier.international-priority-express", + "carrier_name": "FedEx Courier", + "service_name": "International Priority Express" + }, + { + "id": "fedex-courier.overnight", + "carrier_name": "FedEx Courier", + "service_name": "Overnight" + }, + { + "id": "fedex-courier.priority-overnight", + "carrier_name": "FedEx Courier", + "service_name": "Priority Overnight" + }, + { + "id": "fedex-courier.same-day", + "carrier_name": "FedEx Courier", + "service_name": "Same Day" + }, + { + "id": "spadytransportltd-384.standard", + "carrier_name": "SPADY TRANSPORT LTD", + "service_name": "Standard" + }, + { + "id": "tstapi.intermodal", + "carrier_name": "TST", + "service_name": "Intermodal" + }, + { + "id": "tstapi.standard", + "carrier_name": "TST", + "service_name": "Standard" + }, + { + "id": "spanalaska-544.standard", + "carrier_name": "Span Alaska", + "service_name": "Standard" + }, + { + "id": "sunshinelogistics-321.standard", + "carrier_name": "Sunshine Logistics", + "service_name": "Standard" + }, + { + "id": "yellowlogistics-525.standard", + "carrier_name": "Yellow Logistics", + "service_name": "Standard" + }, + { + "id": "abffreight-455.standard", + "carrier_name": "ABF Freight", + "service_name": "Standard" + }, + { + "id": "fastexact-585.standard", + "carrier_name": "Fast Exact", + "service_name": "Standard" + }, + { + "id": "gardewinegroupinc-424.standard", + "carrier_name": "Gardewine Group Inc.", + "service_name": "Standard" + }, + { + "id": "saia.standard", + "carrier_name": "Saia Motor Freight", + "service_name": "Standard" + }, + { + "id": "roadridertransportltd-417.standard", + "carrier_name": "ROAD RIDER Transport Ltd", + "service_name": "Standard" + }, + { + "id": "cct.expedited", + "carrier_name": "CCT", + "service_name": "Expedited" + }, + { + "id": "cct.intermodal", + "carrier_name": "CCT", + "service_name": "Intermodal" + }, + { + "id": "dependablehawaiianexpress-567.standard", + "carrier_name": "Dependable Hawaiian Express", + "service_name": "Standard" + }, + { + "id": "interpacifictransport-457.standard", + "carrier_name": "Inter-Pacific Transport", + "service_name": "Standard" + }, + { + "id": "oneforfreight-481.standard", + "carrier_name": "ONE For Freight", + "service_name": "Standard" + }, + { + "id": "seawayexpress-431.standard", + "carrier_name": "Seaway Express", + "service_name": "Standard" + }, + { + "id": "allpointsfreightinc-546.standard", + "carrier_name": "ALL POINTS FREIGHT INC.", + "service_name": "Standard" + }, + { + "id": "groupelafrance-434.standard", + "carrier_name": "Groupe Lafrance", + "service_name": "Standard" + }, + { + "id": "hiltontransportation-317.standard", + "carrier_name": "Hilton Transportation", + "service_name": "Standard" + }, + { + "id": "locomoteexpress-538.standard", + "carrier_name": "Locomote Express", + "service_name": "Standard" + }, + { + "id": "sendr-603.standard", + "carrier_name": "SENDR", + "service_name": "Standard" + }, + { + "id": "airinuitcargo-539.standard", + "carrier_name": "Air Inuit Cargo", + "service_name": "Standard" + }, + { + "id": "cmttransportinc-628.standard", + "carrier_name": "C M T TRANSPORT INC.", + "service_name": "Standard" + }, + { + "id": "galaxyfreightline-221.standard", + "carrier_name": "Galaxy Freightline", + "service_name": "Standard" + }, + { + "id": "purolatorfreight-577.standard", + "carrier_name": "Purolator Freight", + "service_name": "Standard" + }, + { + "id": "mcdispatch-467.standard", + "carrier_name": "MC Dispatch", + "service_name": "Standard" + }, + { + "id": "midlandtransport-437.standard", + "carrier_name": "Midland Transport", + "service_name": "Standard" + }, + { + "id": "dukesfreightservices-617.standard", + "carrier_name": "Dukes Freight Services", + "service_name": "Standard" + }, + { + "id": "frontiersupplychainsolutions-451.standard", + "carrier_name": "Frontier Supply Chain Solutions", + "service_name": "Standard" + }, + { + "id": "gls-freight.ground", + "carrier_name": "GLS Freight", + "service_name": "Ground" + }, + { + "id": "loadkingtransportinc-385.standard", + "carrier_name": "LOAD KING TRANSPORT INC", + "service_name": "Standard" + }, + { + "id": "dayrossfreightcanada-559.standard", + "carrier_name": "Day & Ross Freight | Canada", + "service_name": "Standard" + }, + { + "id": "gillcologistics-601.standard", + "carrier_name": "Gillco Logistics", + "service_name": "Standard" + }, + { + "id": "reddaway-402.standard", + "carrier_name": "Reddaway", + "service_name": "Standard" + }, + { + "id": "xpo.standard", + "carrier_name": "XPO", + "service_name": "Standard" + }, + { + "id": "lodextransportltd-572.standard", + "carrier_name": "LODEX TRANSPORT LTD", + "service_name": "Standard" + }, + { + "id": "moto.standard", + "carrier_name": "Moto Transportation Services Corp", + "service_name": "Standard" + }, + { + "id": "schneidernational-501.standard", + "carrier_name": "Schneider National", + "service_name": "Standard" + }, + { + "id": "glsfreight-186.standard", + "carrier_name": "GLS Freight", + "service_name": "Standard" + }, + { + "id": "loadlandlogisticsinc-607.standard", + "carrier_name": "LOAD LAND LOGISTICS INC.", + "service_name": "Standard" + }, + { + "id": "pacificcoastexpress-373.standard", + "carrier_name": "Pacific Coast Express", + "service_name": "Standard" + }, + { + "id": "purolatorcourier-401.standard", + "carrier_name": "Purolator Courier", + "service_name": "Standard" + }, + { + "id": "albrighttruckinginc-454.standard", + "carrier_name": "Albright Trucking Inc.", + "service_name": "Standard" + }, + { + "id": "beyondtransportation-285.standard", + "carrier_name": "Beyond Transportation", + "service_name": "Standard" + }, + { + "id": "dinkaenterprises-630.standard", + "carrier_name": "DINKA Enterprises", + "service_name": "Standard" + }, + { + "id": "gls-us.am-select-8a-12p", + "carrier_name": "GLS US", + "service_name": "AM Select 8a-12p" + }, + { + "id": "gls-us.early-priority-overnight", + "carrier_name": "GLS US", + "service_name": "Early Priority Overnight" + }, + { + "id": "gls-us.early-saturday-delivery", + "carrier_name": "GLS US", + "service_name": "Early Saturday Delivery" + }, + { + "id": "gls-us.evening-select-4p-8p", + "carrier_name": "GLS US", + "service_name": "Evening Select 4p-8p" + }, + { + "id": "gls-us.gls-ground", + "carrier_name": "GLS US", + "service_name": "GLS Ground" + }, + { + "id": "gls-us.noon-priority-overnight-sds–saturday-delivery", + "carrier_name": "GLS US", + "service_name": "Noon Priority Overnight SDS – Saturday Delivery" + }, + { + "id": "gls-us.pm-select-12p-4p", + "carrier_name": "GLS US", + "service_name": "PM Select 12p-4p" + }, + { + "id": "gls-us.priority-overnight", + "carrier_name": "GLS US", + "service_name": "Priority Overnight" + }, + { + "id": "gls-us.saturday-delivery", + "carrier_name": "GLS US", + "service_name": "Saturday Delivery" + }, + { + "id": "wsbellcartage-584.standard", + "carrier_name": "W.S. Bell Cartage", + "service_name": "Standard" + }, + { + "id": "manitoulintransport-440.standard", + "carrier_name": "Manitoulin Transport", + "service_name": "Standard" + }, + { + "id": "arbaztransportincdbakrgtransport-415.standard", + "carrier_name": "ARBAZ TRANSPORT INC. DBA KRG TRANSPORT", + "service_name": "Standard" + }, + { + "id": "garantlogisticsltd-548.standard", + "carrier_name": "GARANT LOGISTICS LTD", + "service_name": "Standard" + }, + { + "id": "highlightmotorgroup-542.standard", + "carrier_name": "HIGHLIGHT MOTOR GROUP", + "service_name": "Standard" + }, + { + "id": "kaynortransport-508.standard", + "carrier_name": "KAYNOR TRANSPORT", + "service_name": "Standard" + }, + { + "id": "rpmtransit-356.standard", + "carrier_name": "RPM Transit", + "service_name": "Standard" + }, + { + "id": "bestbaylogistics-532.standard", + "carrier_name": "BEST BAY LOGISTICS", + "service_name": "Standard" + }, + { + "id": "csatransportation-191.standard", + "carrier_name": "CSA Transportation", + "service_name": "Standard" + }, + { + "id": "himmatlogistics-587.standard", + "carrier_name": "HIMMAT LOGISTICS", + "service_name": "Standard" + }, + { + "id": "pacificnorthwestfreightsytems-560.standard", + "carrier_name": "Pacific Northwest Freight Sytems", + "service_name": "Standard" + }, + { + "id": "bowlinggreenlogistics-466.standard", + "carrier_name": "BOWLING GREEN LOGISTICS", + "service_name": "Standard" + }, + { + "id": "eknaamshippingcorp-512.standard", + "carrier_name": "EK NAAM SHIPPING CORP.", + "service_name": "Standard" + }, + { + "id": "frontline.standard", + "carrier_name": "Frontline", + "service_name": "Standard" + }, + { + "id": "jmtransitinc-439.standard", + "carrier_name": "J.M. Transit Inc.", + "service_name": "Standard" + }, + { + "id": "midland.econoline", + "carrier_name": "MidLand Transport", + "service_name": "Econo Line" + }, + { + "id": "midland.standard", + "carrier_name": "MidLand Transport", + "service_name": "Standard" + }, + { + "id": "mjexpress-380.standard", + "carrier_name": "MJ Express", + "service_name": "Standard" + }, + { + "id": "robusttransportservicesltd-479.standard", + "carrier_name": "ROBUST TRANSPORT SERVICES LTD", + "service_name": "Standard" + }, + { + "id": "aonetransport2007ltd-625.standard", + "carrier_name": "A-One Transport (2007) Ltd.", + "service_name": "Standard" + }, + { + "id": "atlaslogistics-262.standard", + "carrier_name": "Atlas Logistics", + "service_name": "Standard" + }, + { + "id": "gurshanlogistics-486.standard", + "carrier_name": "Gurshan Logistics", + "service_name": "Standard" + }, + { + "id": "jdtransportltd-456.standard", + "carrier_name": "J.D.TRANSPORT LTD", + "service_name": "Standard" + }, + { + "id": "rollsright.standard", + "carrier_name": "Rollsright", + "service_name": "Standard" + }, + { + "id": "speedy.standard", + "carrier_name": "Speedy", + "service_name": "Standard" + }, + { + "id": "ettransport-337.standard", + "carrier_name": "E.T. Transport", + "service_name": "Standard" + }, + { + "id": "fedexfreight-154.standard", + "carrier_name": "FedEx Freight", + "service_name": "Standard" + }, + { + "id": "tstcfexpresscanada-282.standard", + "carrier_name": "TST-CF Express | Canada", + "service_name": "Standard" + }, + { + "id": "suntransportationsystems-476.standard", + "carrier_name": "Sun Transportation Systems", + "service_name": "Standard" + }, + { + "id": "universallogisticssolution-517.standard", + "carrier_name": "UNIVERSAL LOGISTICS SOLUTION", + "service_name": "Standard" + }, + { + "id": "apex.standard", + "carrier_name": "Apex", + "service_name": "Standard" + }, + { + "id": "dayrossfreightcanada-133.standard", + "carrier_name": "Day & Ross Freight | Canada", + "service_name": "Standard" + }, + { + "id": "indocanadiancarriers-386.standard", + "carrier_name": "Indo Canadian Carriers", + "service_name": "Standard" + }, + { + "id": "stitransport-250.standard", + "carrier_name": "STI Transport", + "service_name": "Standard" + }, + { + "id": "stallion.apc-priority-worldwide", + "carrier_name": "Stallion", + "service_name": "APC Priority Worldwide" + }, + { + "id": "stallion.apc-priority-worldwide-tracked", + "carrier_name": "Stallion", + "service_name": "APC Priority Worldwide Tracked" + }, + { + "id": "stallion.economy-usa", + "carrier_name": "Stallion", + "service_name": "Stallion Economy USA" + }, + { + "id": "stallion.express", + "carrier_name": "Stallion", + "service_name": "Stallion Express" + }, + { + "id": "stallion.usps-express-mail", + "carrier_name": "Stallion", + "service_name": "USPS Express Mail" + }, + { + "id": "stallion.usps-first-class-mail", + "carrier_name": "Stallion", + "service_name": "USPS First Class Mail" + }, + { + "id": "stallion.usps-library-mail", + "carrier_name": "Stallion", + "service_name": "USPS Library Mail" + }, + { + "id": "stallion.usps-media-mail", + "carrier_name": "Stallion", + "service_name": "USPS Media Mail" + }, + { + "id": "stallion.usps-parcel-select-ground", + "carrier_name": "Stallion", + "service_name": "USPS Parcel Select Ground" + }, + { + "id": "stallion.usps-priority-mail", + "carrier_name": "Stallion", + "service_name": "USPS Priority Mail" + }, + { + "id": "stallion.usps-priority-mail-express", + "carrier_name": "Stallion", + "service_name": "USPS Priority Mail Express" + }, + { + "id": "transkid.standard", + "carrier_name": "Transkid", + "service_name": "Standard" + }, + { + "id": "versacold-438.standard", + "carrier_name": "VersaCold", + "service_name": "Standard" + }, + { + "id": "vttranssolutionsinc-472.standard", + "carrier_name": "VT TRANS SOLUTIONS INC", + "service_name": "Standard" + }, + { + "id": "bestwaycartage-355.standard", + "carrier_name": "BestWay Cartage", + "service_name": "Standard" + }, + { + "id": "commanderwesttrucking-582.standard", + "carrier_name": "Commander West Trucking", + "service_name": "Standard" + }, + { + "id": "roundthelakesmotorexpress-233.standard", + "carrier_name": "Round the Lakes Motor Express", + "service_name": "Standard" + }, + { + "id": "trafalgarsupplyco-511.standard", + "carrier_name": "Trafalgar Supply Co. ", + "service_name": "Standard" + }, + { + "id": "sunburytransportlimited-478.standard", + "carrier_name": "SUNBURY TRANSPORT LIMITED", + "service_name": "Standard" + }, + { + "id": "winniefreight-549.standard", + "carrier_name": "Winnie Freight", + "service_name": "Standard" + }, + { + "id": "gladiatorriggs-483.standard", + "carrier_name": "GLADIATOR RIGGS", + "service_name": "Standard" + }, + { + "id": "hewingstransportationinc-515.standard", + "carrier_name": "Hewings Transportation Inc.", + "service_name": "Standard" + }, + { + "id": "kbdtransportation-420.standard", + "carrier_name": "KBD TRANSPORTATION ", + "service_name": "Standard" + }, + { + "id": "mopallet.standard", + "carrier_name": "Maritime ", + "service_name": "Pallet Program" + }, + { + "id": "transcomax-504.standard", + "carrier_name": "TranscoMAX", + "service_name": "Standard" + }, + { + "id": "uniteddhillontrucklinesudtl-600.standard", + "carrier_name": "UNITED DHILLON TRUCK LINES (UDTL)", + "service_name": "Standard" + }, + { + "id": "atripcodeliveryservice-279.standard", + "carrier_name": "Atripco Delivery Service", + "service_name": "Standard" + }, + { + "id": "interloadtruckserviceltd-569.standard", + "carrier_name": "INTERLOAD TRUCK SERVICE LTD.", + "service_name": "Standard" + }, + { + "id": "kindersley.expedited", + "carrier_name": "Kindersley", + "service_name": "Expedited" + }, + { + "id": "kindersley.intermodal", + "carrier_name": "Kindersley", + "service_name": "Intermodal" + }, + { + "id": "kindersley.standard", + "carrier_name": "Kindersley", + "service_name": "Regular" + }, + { + "id": "polarisweb.standard", + "carrier_name": "Polaris FPP", + "service_name": "Pallet Program" + }, + { + "id": "alainnormandtransportinc-595.standard", + "carrier_name": "ALAIN NORMAND TRANSPORT INC.", + "service_name": "Standard" + }, + { + "id": "ariesgloballogisticsinc-135.standard", + "carrier_name": "Aries Global Logistics Inc", + "service_name": "Standard" + }, + { + "id": "dhillontransport-550.standard", + "carrier_name": "Dhillon Transport", + "service_name": "Standard" + }, + { + "id": "springcreekcarriersinc-291.standard", + "carrier_name": "Spring Creek Carriers Inc", + "service_name": "Standard" + }, + { + "id": "ilclogistics-315.standard", + "carrier_name": "ILC Logistics", + "service_name": "Standard" + }, + { + "id": "transontarioexpress-248.standard", + "carrier_name": "Trans-Ontario Express", + "service_name": "Standard" + }, + { + "id": "yrc.accelerated", + "carrier_name": "YRC", + "service_name": "Accelerated" + }, + { + "id": "yrc.freight-canada-to-us", + "carrier_name": "YRC", + "service_name": "Canada to US" + }, + { + "id": "yrc.freight-dedicated-equipment", + "carrier_name": "YRC", + "service_name": "Dedicated Equipment" + }, + { + "id": "yrc.standard", + "carrier_name": "YRC", + "service_name": "Standard" + }, + { + "id": "yrc.time-critical-by-5pm", + "carrier_name": "YRC", + "service_name": "Critical by 5 PM" + }, + { + "id": "yrc.time-critical-by-afternoon", + "carrier_name": "YRC", + "service_name": "Critical by Afternoon" + }, + { + "id": "yrc.time-critical-fastest-ground", + "carrier_name": "YRC", + "service_name": "Critical Fastest Ground" + }, + { + "id": "yrc.time-critical-hour-window", + "carrier_name": "YRC", + "service_name": "Critical Hour Window" + }, + { + "id": "b2bfreightwayinc-605.standard", + "carrier_name": "B2B Freightway Inc.", + "service_name": "Standard" + }, + { + "id": "caribootruckterminalsltd-616.standard", + "carrier_name": "Cariboo Truck Terminals Ltd", + "service_name": "Standard" + }, + { + "id": "eximlogisticsinc-421.standard", + "carrier_name": "ExIm Logistics Inc.", + "service_name": "Standard" + }, + { + "id": "ics.ground", + "carrier_name": "ICS", + "service_name": "Ground" + }, + { + "id": "ics.next-day", + "carrier_name": "ICS", + "service_name": "Next day" + }, + { + "id": "tstcfexpresssaia-185.standard", + "carrier_name": "TST-CF Express | SAIA", + "service_name": "Standard" + }, + { + "id": "comoxpacificexpress-257.standard", + "carrier_name": "Comox Pacific Express", + "service_name": "Standard" + }, + { + "id": "galaxyfreightlineinc-183.standard", + "carrier_name": "Galaxy Freightline Inc", + "service_name": "Standard" + }, + { + "id": "samoyedtransport-602.standard", + "carrier_name": "Samoyed Transport", + "service_name": "Standard" + }, + { + "id": "sprinterdeliveryltd-522.standard", + "carrier_name": "Sprinter Delivery Ltd.", + "service_name": "Standard" + }, + { + "id": "gtboltoninc-491.standard", + "carrier_name": "G.T. BOLTON INC.", + "service_name": "Standard" + }, + { + "id": "polarisdirect.standard", + "carrier_name": "Polaris", + "service_name": "Direct" + }, + { + "id": "ups.3day-select", + "carrier_name": "UPS", + "service_name": "3 Day Select" + }, + { + "id": "ups.expedited", + "carrier_name": "UPS", + "service_name": "Expedited" + }, + { + "id": "ups.express", + "carrier_name": "UPS", + "service_name": "Express" + }, + { + "id": "ups.express-early", + "carrier_name": "UPS", + "service_name": "Express Early" + }, + { + "id": "ups.express-saver", + "carrier_name": "UPS", + "service_name": "Express Saver" + }, + { + "id": "ups.ground", + "carrier_name": "UPS", + "service_name": "Ground" + }, + { + "id": "ups.standard", + "carrier_name": "UPS", + "service_name": "Standard" + }, + { + "id": "ups.worldwide-expedited", + "carrier_name": "UPS", + "service_name": "Worldwide Expedited" + }, + { + "id": "ups.worldwide-express", + "carrier_name": "UPS", + "service_name": "Worldwide Express" + }, + { + "id": "ups.worldwide-express-plus", + "carrier_name": "UPS", + "service_name": "Worldwide Express Plus" + }, + { + "id": "ups.worldwide-express-saver", + "carrier_name": "UPS", + "service_name": "Worldwide Express Saver" + }, + { + "id": "argustransportcanada-463.standard", + "carrier_name": "ARGUS Transport Canada", + "service_name": "Standard" + }, + { + "id": "crosscountryfreightsolutions-571.standard", + "carrier_name": "CrossCountry Freight Solutions", + "service_name": "Standard" + }, + { + "id": "geysertransportltd-606.standard", + "carrier_name": "Geyser Transport Ltd", + "service_name": "Standard" + }, + { + "id": "greysertransportltd-606.standard", + "carrier_name": "Greyser Transport Ltd", + "service_name": "Standard" + }, + { + "id": "steelestransportationgroup-580.standard", + "carrier_name": "Steele's Transportation Group", + "service_name": "Standard" + }, + { + "id": "titantranslineinc-450.standard", + "carrier_name": "Titan Transline Inc.", + "service_name": "Standard" + }, + { + "id": "2stopdelivery-393.standard", + "carrier_name": "2 STOP DELIVERY", + "service_name": "Standard" + }, + { + "id": "aduiepyleinc-589.standard", + "carrier_name": "A Duie Pyle, Inc", + "service_name": "Standard" + }, + { + "id": "centralislanddistributors-554.standard", + "carrier_name": "Central Island Distributors", + "service_name": "Standard" + }, + { + "id": "minimaxexpress-418.standard", + "carrier_name": "Minimax Express", + "service_name": "Standard" + }, + { + "id": "dayrosscommerce-266.standard", + "carrier_name": "Day & Ross Commerce", + "service_name": "Standard" + }, + { + "id": "highlightmotorgroup-500.standard", + "carrier_name": "HIGHLIGHT MOTOR GROUP", + "service_name": "Standard" + }, + { + "id": "internordtransportation-461.standard", + "carrier_name": "Inter-Nord Transportation", + "service_name": "Standard" + }, + { + "id": "westmancourier-419.standard", + "carrier_name": "Westman Courier", + "service_name": "Standard" + } + ], + "DEV_SERVICES": [ + { + "id": "kindersley-freight.domestic-expedited", + "carrier_name": "Kindersley Transport", + "service_name": "Domestic Expedited" + }, + { + "id": "kindersley-freight.domestic-rail", + "carrier_name": "Kindersley Transport", + "service_name": "Domestic Rail" + }, + { + "id": "kindersley-freight.domestic-road", + "carrier_name": "Kindersley Transport", + "service_name": "Domestic Road" + }, + { + "id": "kindersley-freight.transborder", + "carrier_name": "Kindersley Transport", + "service_name": "Transborder" + }, + { + "id": "newpennmotorexpress-445.standard", + "carrier_name": "New Penn Motor Express", + "service_name": "Standard" + }, + { + "id": "a10cD2MfJjiOCcRejjgv3cHMyRcRBhE3.standard", + "carrier_name": "Bill's Spot Carrier", + "service_name": "Standard" + }, + { + "id": "apps.intermodal", + "carrier_name": "APPS", + "service_name": "Intermodal" + }, + { + "id": "apps.standard", + "carrier_name": "APPS", + "service_name": "Standard" + }, + { + "id": "dayrosscommerce-266.standard", + "carrier_name": "Day & Ross Commerce", + "service_name": "Standard" + }, + { + "id": "fedex-courier.2-day", + "carrier_name": "FedEx Courier", + "service_name": "2-Day" + }, + { + "id": "fedex-courier.2-day-a-m", + "carrier_name": "FedEx Courier", + "service_name": "2-Day AM" + }, + { + "id": "fedex-courier.express-saver", + "carrier_name": "FedEx Courier", + "service_name": "Express Saver" + }, + { + "id": "fedex-courier.first-overnight", + "carrier_name": "FedEx Courier", + "service_name": "First Overnight" + }, + { + "id": "fedex-courier.ground", + "carrier_name": "FedEx Courier", + "service_name": "Ground" + }, + { + "id": "fedex-courier.international-connect-plus", + "carrier_name": "FedEx Courier", + "service_name": "International Connect Plus" + }, + { + "id": "fedex-courier.international-economy", + "carrier_name": "FedEx Courier", + "service_name": "International Economy" + }, + { + "id": "fedex-courier.international-ground", + "carrier_name": "FedEx Courier", + "service_name": "International Ground" + }, + { + "id": "fedex-courier.international-priority", + "carrier_name": "FedEx Courier", + "service_name": "International Priority" + }, + { + "id": "fedex-courier.international-priority-express", + "carrier_name": "FedEx Courier", + "service_name": "International Priority Express" + }, + { + "id": "fedex-courier.overnight", + "carrier_name": "FedEx Courier", + "service_name": "Overnight" + }, + { + "id": "fedex-courier.priority-overnight", + "carrier_name": "FedEx Courier", + "service_name": "Priority Overnight" + }, + { + "id": "fedex-courier.same-day", + "carrier_name": "FedEx Courier", + "service_name": "Same Day" + }, + { + "id": "fedexfreight-154.standard", + "carrier_name": "FedEx Freight", + "service_name": "Standard" + }, + { + "id": "gsm.air-skip", + "carrier_name": "GTA GSM", + "service_name": "AirSkip" + }, + { + "id": "gsm.air-skip-eco", + "carrier_name": "GTA GSM", + "service_name": "AirSkipEco" + }, + { + "id": "gsm.air-skip-plus", + "carrier_name": "GTA GSM", + "service_name": "AirSkip+" + }, + { + "id": "gsm.armed-secure-air", + "carrier_name": "GTA GSM", + "service_name": "Armed Secure Air" + }, + { + "id": "gsm.armed-secure-ground", + "carrier_name": "GTA GSM", + "service_name": "Armed Secure Ground" + }, + { + "id": "gsm.ground", + "carrier_name": "GTA GSM", + "service_name": "Ground" + }, + { + "id": "gsm.secure-air", + "carrier_name": "GTA GSM", + "service_name": "Secure Air" + }, + { + "id": "gsm.secure-ground", + "carrier_name": "GTA GSM", + "service_name": "Secure Ground" + }, + { + "id": "gsm.zone-skip", + "carrier_name": "GTA GSM", + "service_name": "ZoneSkip" + }, + { + "id": "gsm.zone-skip-plus", + "carrier_name": "GTA GSM", + "service_name": "ZoneSkip+" + }, + { + "id": "dhlexpress.domestic-express", + "carrier_name": "DHL Express", + "service_name": "Domestic Express" + }, + { + "id": "dhlexpress.domestic-express1030am", + "carrier_name": "DHL Express", + "service_name": "Domestic Express 10:30" + }, + { + "id": "dhlexpress.domestic-express9am", + "carrier_name": "DHL Express", + "service_name": "Domestic Express 9:00" + }, + { + "id": "dhlexpress.economy-select", + "carrier_name": "DHL Express", + "service_name": "Economy Select" + }, + { + "id": "dhlexpress.express-easy", + "carrier_name": "DHL Express", + "service_name": "Express Easy" + }, + { + "id": "dhlexpress.express-worldwide", + "carrier_name": "DHL Express", + "service_name": "Express Worldwide" + }, + { + "id": "dhlexpress.express1030am", + "carrier_name": "DHL Express", + "service_name": "Express 10:30" + }, + { + "id": "dhlexpress.express12pm", + "carrier_name": "DHL Express", + "service_name": "Express 12:00" + }, + { + "id": "dhlexpress.express9am", + "carrier_name": "DHL Express", + "service_name": "Express 9:00" + }, + { + "id": "nin.next-day", + "carrier_name": "NIN", + "service_name": "Next Day" + }, + { + "id": "nin.same-day", + "carrier_name": "NIN", + "service_name": "Same Day" + }, + { + "id": "one.standard", + "carrier_name": "ONE Transportation", + "service_name": "Standard" + }, + { + "id": "rollsright.standard", + "carrier_name": "Rollsright", + "service_name": "Standard" + }, + { + "id": "stallion.apc-priority-worldwide", + "carrier_name": "Stallion", + "service_name": "APC Priority Worldwide" + }, + { + "id": "stallion.apc-priority-worldwide-tracked", + "carrier_name": "Stallion", + "service_name": "APC Priority Worldwide Tracked" + }, + { + "id": "stallion.economy-usa", + "carrier_name": "Stallion", + "service_name": "Stallion Economy USA" + }, + { + "id": "stallion.express", + "carrier_name": "Stallion", + "service_name": "Stallion Express" + }, + { + "id": "stallion.usps-express-mail", + "carrier_name": "Stallion", + "service_name": "USPS Express Mail" + }, + { + "id": "stallion.usps-first-class-mail", + "carrier_name": "Stallion", + "service_name": "USPS First Class Mail" + }, + { + "id": "stallion.usps-library-mail", + "carrier_name": "Stallion", + "service_name": "USPS Library Mail" + }, + { + "id": "stallion.usps-media-mail", + "carrier_name": "Stallion", + "service_name": "USPS Media Mail" + }, + { + "id": "stallion.usps-parcel-select-ground", + "carrier_name": "Stallion", + "service_name": "USPS Parcel Select Ground" + }, + { + "id": "stallion.usps-priority-mail", + "carrier_name": "Stallion", + "service_name": "USPS Priority Mail" + }, + { + "id": "stallion.usps-priority-mail-express", + "carrier_name": "Stallion", + "service_name": "USPS Priority Mail Express" + }, + { + "id": "wuPUFbL2xJpG1vd4jHDl0vaXiXZ99WO4.standard", + "carrier_name": "test manager add", + "service_name": "Standard" + }, + { + "id": "apexmotorexpress-258.standard", + "carrier_name": "Apex Motor Express", + "service_name": "Standard" + }, + { + "id": "bettertrucks.ddu", + "carrier_name": "Better Trucks", + "service_name": "DDU" + }, + { + "id": "bettertrucks.express", + "carrier_name": "Better Trucks", + "service_name": "Express" + }, + { + "id": "bettertrucks.next_day", + "carrier_name": "Better Trucks", + "service_name": "Next Day" + }, + { + "id": "bettertrucks.same_day", + "carrier_name": "Better Trucks", + "service_name": "Same Day" + }, + { + "id": "IQ1rjQyWY8EflOBKVyqGM1syqer1O65V.standard", + "carrier_name": "Carrier Ben", + "service_name": "Standard" + }, + { + "id": "kindersley.expedited", + "carrier_name": "Kindersley", + "service_name": "Expedited" + }, + { + "id": "kindersley.intermodal", + "carrier_name": "Kindersley", + "service_name": "Intermodal" + }, + { + "id": "kindersley.standard", + "carrier_name": "Kindersley", + "service_name": "Regular" + }, + { + "id": "speedy.standard", + "carrier_name": "Speedy", + "service_name": "Standard" + }, + { + "id": "vitran.maxx", + "carrier_name": "Vitran", + "service_name": "Maxx" + }, + { + "id": "vitran.priority", + "carrier_name": "Vitran", + "service_name": "Priority" + }, + { + "id": "vitran.regular", + "carrier_name": "Vitran", + "service_name": "Regular" + }, + { + "id": "c5itKPJ6v6s4cmHEhjMDbbp9XD5lKoPR.standard", + "carrier_name": "Bill's Truck Shop", + "service_name": "Standard" + }, + { + "id": "caledoncouriers-294.standard", + "carrier_name": "Caledon Couriers", + "service_name": "Standard" + }, + { + "id": "dayton-freight.standard", + "carrier_name": "Dayton Freight", + "service_name": "Standard" + }, + { + "id": "dj9Lv4FUhkBIG7tdKiWh4n3U2lP2Drc5.standard", + "carrier_name": "testNewCarrierGar", + "service_name": "Standard" + }, + { + "id": "hiway.standard", + "carrier_name": "Hi-Way9", + "service_name": "Standard" + }, + { + "id": "boxknight.next-day", + "carrier_name": "BoxKnight", + "service_name": "Next Day" + }, + { + "id": "boxknight.sameday", + "carrier_name": "BoxKnight", + "service_name": "Same Day" + }, + { + "id": "cct.expedited", + "carrier_name": "CCT", + "service_name": "Expedited" + }, + { + "id": "cct.intermodal", + "carrier_name": "CCT", + "service_name": "Intermodal" + }, + { + "id": "peglobal-511.standard", + "carrier_name": "PE Global", + "service_name": "Standard" + }, + { + "id": "polarisdirect.standard", + "carrier_name": "Polaris", + "service_name": "Direct" + }, + { + "id": "billatransport-203.standard", + "carrier_name": "Billa Transport", + "service_name": "Standard" + }, + { + "id": "purolatorfreight-151.standard", + "carrier_name": "Purolator Freight ", + "service_name": "Standard" + }, + { + "id": "wce.standard", + "carrier_name": "WCE", + "service_name": "Intermodal" + }, + { + "id": "ajWA9sU9dV6U6oSB5GrokBvlzfV2RRf7.standard", + "carrier_name": "Garima-test", + "service_name": "Standard" + }, + { + "id": "c19h151C0I4Sdnp3Mqt0jRLxjTcNtksA.standard", + "carrier_name": "BinduCarrier", + "service_name": "Standard" + }, + { + "id": "FzXhDtSYDgMYP97Zi28GgU4j8QNXBllS.standard", + "carrier_name": "Apex-SPOT", + "service_name": "Standard" + }, + { + "id": "holland.guaranteed-330-pm", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed by 3:30 PM" + }, + { + "id": "holland.guaranteed-9-am", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed by 9:00 AM" + }, + { + "id": "holland.guaranteed-day", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed Day" + }, + { + "id": "holland.guaranteed-hour", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed Hour" + }, + { + "id": "holland.guaranteed-multi-hour", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed Multi Hour" + }, + { + "id": "holland.guaranteed-noon", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed by Noon" + }, + { + "id": "holland.guaranteed-weekend", + "carrier_name": "Holland Freight", + "service_name": "Guaranteed Weekend" + }, + { + "id": "holland.inter-regional", + "carrier_name": "Holland Freight", + "service_name": "Inter-Regional" + }, + { + "id": "holland.interline", + "carrier_name": "Holland Freight", + "service_name": "Interline" + }, + { + "id": "holland.regional", + "carrier_name": "Holland Freight", + "service_name": "Regional" + }, + { + "id": "LfT69HBpfJrjS9wv0tkFVylVxcMwWIVt.standard", + "carrier_name": "test create 2", + "service_name": "Standard" + }, + { + "id": "mopallet.standard", + "carrier_name": "Maritime ", + "service_name": "Pallet Program" + }, + { + "id": "bgxtransportation-361.standard", + "carrier_name": "BGX Transportation", + "service_name": "Standard" + }, + { + "id": "ei8AJgRFI3Xslob8UmFawLq71jhlKyZX.standard", + "carrier_name": "Service Test", + "service_name": "Standard" + }, + { + "id": "vankam.standard", + "carrier_name": "Vankam", + "service_name": "Standard" + }, + { + "id": "AbqB0FuehDpOtTxHzhhEvBs8CRJ44Z2I.standard", + "carrier_name": "Manager Carrier", + "service_name": "Standard" + }, + { + "id": "bbWZeWwm92b0eyQBYnRNT5PUJb7vslCY.standard", + "carrier_name": "1Manager", + "service_name": "Standard" + }, + { + "id": "dhl-ecomm.packet-international", + "carrier_name": "DHL eCommerce", + "service_name": "Package International" + }, + { + "id": "dhl-ecomm.parcel-expedited", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel Expedited" + }, + { + "id": "dhl-ecomm.parcel-expedited-max", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel Expedited Max" + }, + { + "id": "dhl-ecomm.parcel-ground", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel Ground" + }, + { + "id": "dhl-ecomm.parcel-international-direct", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel International Direct" + }, + { + "id": "dhl-ecomm.parcel-international-direct-priority", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel International Direct Priority" + }, + { + "id": "dhl-ecomm.parcel-international-direct-standard", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel International Direct Standard" + }, + { + "id": "dhl-ecomm.parcel-international-standard", + "carrier_name": "DHL eCommerce", + "service_name": "Parcel International Standard" + }, + { + "id": "saia.standard", + "carrier_name": "Saia Motor Freight", + "service_name": "Standard" + }, + { + "id": "Mg5oX7RWPCrENCdFkqaijO3NI0qKLFPt.standard", + "carrier_name": "2garima", + "service_name": "Standard" + }, + { + "id": "moto.standard", + "carrier_name": "Moto Transportation Services Corp", + "service_name": "Standard" + }, + { + "id": "2vlSmNNDnF6OWQFjUTFPlwLpTv0RkgXp.standard", + "carrier_name": "New Carrier1", + "service_name": "Standard" + }, + { + "id": "6D24l95xtm4jFC550GYo0tLEBrjgavVS.standard", + "carrier_name": "Admin Added carrier", + "service_name": "Standard" + }, + { + "id": "ab-courier.canada-1030am", + "carrier_name": "A&B Courier", + "service_name": "Canada 10:30am" + }, + { + "id": "ab-courier.canada-930am", + "carrier_name": "A&B Courier", + "service_name": "Canada 9:30am" + }, + { + "id": "ab-courier.canada-ground", + "carrier_name": "A&B Courier", + "service_name": "Canada Ground" + }, + { + "id": "ab-courier.canada-overnight", + "carrier_name": "A&B Courier", + "service_name": "Canada Overnight" + }, + { + "id": "ab-courier.direct", + "carrier_name": "A&B Courier", + "service_name": "Direct" + }, + { + "id": "ab-courier.four-hour", + "carrier_name": "A&B Courier", + "service_name": "Four Hour" + }, + { + "id": "ab-courier.rush", + "carrier_name": "A&B Courier", + "service_name": "Rush" + }, + { + "id": "ab-courier.sameday", + "carrier_name": "A&B Courier", + "service_name": "Sameday" + }, + { + "id": "ab-courier.usa-ground", + "carrier_name": "A&B Courier", + "service_name": "USA Ground" + }, + { + "id": "daynross.cs", + "carrier_name": "Day & Ross", + "service_name": "CS" + }, + { + "id": "daynross.domestic-standard", + "carrier_name": "Day & Ross", + "service_name": "Domestic Standard" + }, + { + "id": "daynross.transborder-standard", + "carrier_name": "Day & Ross", + "service_name": "Transborder Standard" + }, + { + "id": "dayrossrlc-132.standard", + "carrier_name": "Day & Ross RLC", + "service_name": "Standard" + }, + { + "id": "LtViz90ze09z34gcXmBbnFhSSqvvO6h5.standard", + "carrier_name": "FastfrateTest", + "service_name": "Standard" + }, + { + "id": "B6lmGiW6bT5uYz0ceJwoqduC0QRf51cr.standard", + "carrier_name": "1NewDinesh", + "service_name": "Standard" + }, + { + "id": "fedex.economy", + "carrier_name": "FedEx Freight", + "service_name": "Economy" + }, + { + "id": "fedex.standard", + "carrier_name": "FedEx Freight", + "service_name": "Priority" + }, + { + "id": "VwSa0UuckhcLkEzVoCSkhPJoTygfzeIk.standard", + "carrier_name": "Insurance Testing Carrier", + "service_name": "Standard" + }, + { + "id": "YlhNxO31X2UtvtKauwVLdIo4skgYEtDi.standard", + "carrier_name": "Dino Trucking Inc", + "service_name": "Standard" + }, + { + "id": "anq7q2st2UWtnj2WPWqXtgIgaUa4AZF4.standard", + "carrier_name": "Air lines ", + "service_name": "Standard" + }, + { + "id": "dhlcanada-230.standard", + "carrier_name": "DHL CANADA", + "service_name": "Standard" + }, + { + "id": "fedexexpress-272.standard", + "carrier_name": "FedEx Express", + "service_name": "Standard" + }, + { + "id": "KQfppq3g1UUR3BabBSUvQko4MWDUPnul.standard", + "carrier_name": "FC Carrier", + "service_name": "Standard" + }, + { + "id": "speedytransport-153.standard", + "carrier_name": "Speedy Transport", + "service_name": "Standard" + }, + { + "id": "tstapi.intermodal", + "carrier_name": "TST", + "service_name": "Intermodal" + }, + { + "id": "tstapi.standard", + "carrier_name": "TST", + "service_name": "Standard" + }, + { + "id": "ab-courier-ltl.ltl-direct", + "carrier_name": "A&B Courier", + "service_name": "Direct (Pallet)" + }, + { + "id": "ab-courier-ltl.ltl-rush", + "carrier_name": "A&B Courier", + "service_name": "Rush (Pallet)" + }, + { + "id": "ab-courier-ltl.ltl-sameday", + "carrier_name": "A&B Courier", + "service_name": "Sameday (Pallet)" + }, + { + "id": "accordtransportation-176.standard", + "carrier_name": "Accord Transportation", + "service_name": "Standard" + }, + { + "id": "am0cHEYPS0pZbXDbVXUM6opDv3c4YyxP.standard", + "carrier_name": "testGar", + "service_name": "Standard" + }, + { + "id": "purolatorcourier-401.standard", + "carrier_name": "Purolator Courier", + "service_name": "Standard" + }, + { + "id": "swyft.nextday", + "carrier_name": "Swyft", + "service_name": "Next Day" + }, + { + "id": "swyft.sameday", + "carrier_name": "Swyft", + "service_name": "Same Day" + }, + { + "id": "upscourier-162.standard", + "carrier_name": "UPS Courier", + "service_name": "Standard" + }, + { + "id": "2ya9w4n9GgNbqWlBgKcWro3WYkm0SI2d.standard", + "carrier_name": "SD Carrier", + "service_name": "Standard" + }, + { + "id": "csa.standard", + "carrier_name": "CSA", + "service_name": "Standard" + }, + { + "id": "newpenn.standard", + "carrier_name": "New Penn", + "service_name": "Standard" + }, + { + "id": "spring-gds.spring-direct", + "carrier_name": "Spring GDS", + "service_name": "Spring Direct" + }, + { + "id": "spring-gds.spring-gateway-parcel", + "carrier_name": "Spring GDS", + "service_name": "Spring Gateway Parcel" + }, + { + "id": "spring-gds.spring-packet-plus", + "carrier_name": "Spring GDS", + "service_name": "Spring Packet Plus Registered" + }, + { + "id": "spring-gds.spring-packet-tracked", + "carrier_name": "Spring GDS", + "service_name": "Spring Packet Tracked" + }, + { + "id": "spring-gds.spring-packet-untracked", + "carrier_name": "Spring GDS", + "service_name": "Spring Packet Untracked" + }, + { + "id": "TXbrc3AuAEXSJ9uQnT40gRG0zk41qTml.standard", + "carrier_name": "1Dinesh carrier", + "service_name": "Standard" + }, + { + "id": "6AS1YHCOu0JmUMnb0kEHBAS1blnvdi3C.standard", + "carrier_name": "SushmaCarrier", + "service_name": "Standard" + }, + { + "id": "gls-us.am-select-8a-12p", + "carrier_name": "GLS US", + "service_name": "AM Select 8a-12p" + }, + { + "id": "gls-us.early-priority-overnight", + "carrier_name": "GLS US", + "service_name": "Early Priority Overnight" + }, + { + "id": "gls-us.early-saturday-delivery", + "carrier_name": "GLS US", + "service_name": "Early Saturday Delivery" + }, + { + "id": "gls-us.evening-select-4p-8p", + "carrier_name": "GLS US", + "service_name": "Evening Select 4p-8p" + }, + { + "id": "gls-us.gls-ground", + "carrier_name": "GLS US", + "service_name": "GLS Ground" + }, + { + "id": "gls-us.noon-priority-overnight-sds–saturday-delivery", + "carrier_name": "GLS US", + "service_name": "Noon Priority Overnight SDS – Saturday Delivery" + }, + { + "id": "gls-us.pm-select-12p-4p", + "carrier_name": "GLS US", + "service_name": "PM Select 12p-4p" + }, + { + "id": "gls-us.priority-overnight", + "carrier_name": "GLS US", + "service_name": "Priority Overnight" + }, + { + "id": "gls-us.saturday-delivery", + "carrier_name": "GLS US", + "service_name": "Saturday Delivery" + }, + { + "id": "6XTleMYOSAZrdGo5dkg0AB2FS3E35oMF.standard", + "carrier_name": "Show Demo", + "service_name": "Standard" + }, + { + "id": "abcourier-480.standard", + "carrier_name": "A&B Courier (Manual)", + "service_name": "Standard" + }, + { + "id": "allspeed-432.standard", + "carrier_name": "All Speed", + "service_name": "Standard" + }, + { + "id": "KqwXT5YZbXOvt0PXziMUxkj3J6V1pTsQ.standard", + "carrier_name": "SushmaCarrier", + "service_name": "Standard" + }, + { + "id": "sameday.2-man-delivery-to-entrance", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Delivery to Entrance - 2 Person" + }, + { + "id": "sameday.2-man-delivery-to-room-of-choice", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Delivery to Room of Choice - 2 Person" + }, + { + "id": "sameday.2-man-delivery-to-room-of-choice-with-debris-removal", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Two-person delivery to room of choice with debris removal" + }, + { + "id": "sameday.dayr-ecom-urgent-pac", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "eCommerce Urgent Pak" + }, + { + "id": "sameday.delivery-to-entrance", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Delivery to Entrance" + }, + { + "id": "sameday.delivery-to-room-of-choice", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Delivery to Room of Choice" + }, + { + "id": "sameday.delivery-to-room-of-choice-with-debris-removal", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Delivery to Room of Choice with Debris Removal" + }, + { + "id": "sameday.ground-daynross-road", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Ground - Road" + }, + { + "id": "sameday.next-day-before-5pm", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Next Day Delivery by 5 PM" + }, + { + "id": "sameday.next-day-before-9am", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Next Day Delivery by 9 AM" + }, + { + "id": "sameday.next-day-delivery-before-noon", + "carrier_name": "Day & Ross Commerce Solutions", + "service_name": "Next Day Delivery Before Noon" + }, + { + "id": "transkid.standard", + "carrier_name": "Transkid", + "service_name": "Standard" + }, + { + "id": "37SyVWjCgJxqehNHTdBaFhz1MQxKK0Vd.standard", + "carrier_name": "2New Spot carrier", + "service_name": "Standard" + }, + { + "id": "atlantistransport-347.standard", + "carrier_name": "Atlantis Transport", + "service_name": "Standard" + }, + { + "id": "excel-transport.standard", + "carrier_name": "Excel Transportation", + "service_name": "Standard" + }, + { + "id": "fleet-optics.standard", + "carrier_name": "Fleet Optics", + "service_name": "Standard" + }, + { + "id": "gardewine.standard", + "carrier_name": "Gardewine", + "service_name": "Standard" + }, + { + "id": "ics.ground", + "carrier_name": "ICS", + "service_name": "Ground" + }, + { + "id": "ics.next-day", + "carrier_name": "ICS", + "service_name": "Next day" + }, + { + "id": "usps.first-class", + "carrier_name": "USPS", + "service_name": "First Class" + }, + { + "id": "usps.ground-advantage", + "carrier_name": "USPS", + "service_name": "Ground Advantage" + }, + { + "id": "usps.parcel-select-ground", + "carrier_name": "USPS", + "service_name": "Parcel Select Ground" + }, + { + "id": "usps.priority-mail", + "carrier_name": "USPS", + "service_name": "Priority Mail" + }, + { + "id": "usps.priority-mail-express", + "carrier_name": "USPS", + "service_name": "Priority Mail Express" + }, + { + "id": "ups.3day-select", + "carrier_name": "UPS", + "service_name": "3 Day Select" + }, + { + "id": "ups.expedited", + "carrier_name": "UPS", + "service_name": "Expedited" + }, + { + "id": "ups.express", + "carrier_name": "UPS", + "service_name": "Express" + }, + { + "id": "ups.express-early", + "carrier_name": "UPS", + "service_name": "Express Early" + }, + { + "id": "ups.express-saver", + "carrier_name": "UPS", + "service_name": "Express Saver" + }, + { + "id": "ups.ground", + "carrier_name": "UPS", + "service_name": "Ground" + }, + { + "id": "ups.standard", + "carrier_name": "UPS", + "service_name": "Standard" + }, + { + "id": "ups.worldwide-expedited", + "carrier_name": "UPS", + "service_name": "Worldwide Expedited" + }, + { + "id": "ups.worldwide-express", + "carrier_name": "UPS", + "service_name": "Worldwide Express" + }, + { + "id": "ups.worldwide-express-plus", + "carrier_name": "UPS", + "service_name": "Worldwide Express Plus" + }, + { + "id": "ups.worldwide-express-saver", + "carrier_name": "UPS", + "service_name": "Worldwide Express Saver" + }, + { + "id": "airpro-255.standard", + "carrier_name": "Air Pro", + "service_name": "Standard" + }, + { + "id": "apex.standard", + "carrier_name": "Apex", + "service_name": "Standard" + }, + { + "id": "frontline.standard", + "carrier_name": "Frontline", + "service_name": "Standard" + }, + { + "id": "nationex.standard", + "carrier_name": "Nationex", + "service_name": "Standard" + }, + { + "id": "qM4cPFVXO20tNUz8fuU2bZCd6s0a6oKE.standard", + "carrier_name": "GL Freight", + "service_name": "Standard" + }, + { + "id": "swiftdeliverysystems-268.standard", + "carrier_name": "Swift Delivery Systems", + "service_name": "Standard" + }, + { + "id": "comox.standard", + "carrier_name": "Comox Pacific Express", + "service_name": "Standard" + }, + { + "id": "h9d2M32kjQDeB0GldPN0AMYvhuEXI45c.standard", + "carrier_name": "New2New carrier ", + "service_name": "Standard" + }, + { + "id": "maritime.dry", + "carrier_name": "Maritime", + "service_name": "Dry" + }, + { + "id": "maritime.frozen", + "carrier_name": "Maritime", + "service_name": "Reefer" + }, + { + "id": "maritime.heat", + "carrier_name": "Maritime", + "service_name": "Heat" + }, + { + "id": "overland.standard", + "carrier_name": "Overland", + "service_name": "Standard" + }, + { + "id": "manitoulintransport-440.standard", + "carrier_name": "Manitoulin Transport", + "service_name": "Standard" + }, + { + "id": "tforcefreight.tforcefreight-guarnteed", + "carrier_name": "TForceFreight", + "service_name": "TForceFreight Guaranteed" + }, + { + "id": "tforcefreight.tforcefreight-ltl", + "carrier_name": "TForceFreight", + "service_name": "TForceFreight LTL" + }, + { + "id": "tforcefreight.tforcefreight-standard", + "carrier_name": "TForceFreight", + "service_name": "TForceFreight Standard" + }, + { + "id": "V44U22d1DwvXrhpvW7UtPrZ4GQQ6x6Hb.standard", + "carrier_name": "11Manager Carriers", + "service_name": "Standard" + }, + { + "id": "yrc.accelerated", + "carrier_name": "YRC", + "service_name": "Accelerated" + }, + { + "id": "yrc.freight-canada-to-us", + "carrier_name": "YRC", + "service_name": "Canada to US" + }, + { + "id": "yrc.freight-dedicated-equipment", + "carrier_name": "YRC", + "service_name": "Dedicated Equipment" + }, + { + "id": "yrc.standard", + "carrier_name": "YRC", + "service_name": "Standard" + }, + { + "id": "yrc.time-critical-by-5pm", + "carrier_name": "YRC", + "service_name": "Critical by 5 PM" + }, + { + "id": "yrc.time-critical-by-afternoon", + "carrier_name": "YRC", + "service_name": "Critical by Afternoon" + }, + { + "id": "yrc.time-critical-fastest-ground", + "carrier_name": "YRC", + "service_name": "Critical Fastest Ground" + }, + { + "id": "yrc.time-critical-hour-window", + "carrier_name": "YRC", + "service_name": "Critical Hour Window" + }, + { + "id": "Zs8TafpKGZ0fx9utJ42E4ZFyLtoRUmzE.standard", + "carrier_name": "BinduTestCarrier", + "service_name": "Standard" + }, + { + "id": "abffreight-455.standard", + "carrier_name": "ABF Freight", + "service_name": "Standard" + }, + { + "id": "dayrosscdn-133.standard", + "carrier_name": "Day & Ross CDN", + "service_name": "Standard" + }, + { + "id": "gMS33dZBLDmtyw0ileP8b0wxytNPUNEe.standard", + "carrier_name": "SPOT Polaris", + "service_name": "Standard" + }, + { + "id": "intelcom.standard", + "carrier_name": "Intelcom", + "service_name": "Standard" + }, + { + "id": "maritimeontario-267.standard", + "carrier_name": "Maritime-Ontario", + "service_name": "Standard" + }, + { + "id": "minimax.standard", + "carrier_name": "Minimax", + "service_name": "Standard" + }, + { + "id": "allmodes-147.standard", + "carrier_name": "All Modes", + "service_name": "Standard" + }, + { + "id": "gls-freight.ground", + "carrier_name": "GLS Freight", + "service_name": "Ground" + }, + { + "id": "kindersley-courier.standard", + "carrier_name": "Kindersley Transport", + "service_name": "Standard" + }, + { + "id": "LfuLpu4OaMgNiVnBFYbNUXxiAyY66z3m.standard", + "carrier_name": "Joshuas super advanced and not sketchy at all spot carrier", + "service_name": "Standard" + }, + { + "id": "midland.econoline", + "carrier_name": "MidLand Transport", + "service_name": "Econo Line" + }, + { + "id": "midland.reefer", + "carrier_name": "MidLand Transport", + "service_name": "Reefer" + }, + { + "id": "midland.standard", + "carrier_name": "MidLand Transport", + "service_name": "Standard" + }, + { + "id": "TKzEkXVCaQIkGU0SqHyix6Lujv1YiCaR.standard", + "carrier_name": "Test carrier", + "service_name": "Standard" + }, + { + "id": "1lFVuiEgI85Be3BmNH9Jc8QKe5DEDVs0.standard", + "carrier_name": "vicky", + "service_name": "Standard" + }, + { + "id": "gY2Ah4O53ACmkb6D75rHLg6eMfzpsIpw.standard", + "carrier_name": "Flex transport", + "service_name": "Standard" + }, + { + "id": "morneau.standard", + "carrier_name": "Morneau Transport", + "service_name": "Standard" + }, + { + "id": "4TBSrW3F8aUzohICK0A0XH2mk25Aipdx.standard", + "carrier_name": "Bill Cartage", + "service_name": "Standard" + }, + { + "id": "nishantransportinc-423.standard", + "carrier_name": "NISHAN TRANSPORT INC.", + "service_name": "Standard" + }, + { + "id": "xpologistics-265.standard", + "carrier_name": "XPO Logistics", + "service_name": "Standard" + }, + { + "id": "Fqrdds8xTJcw2gopMEKjgdNR0jTkxxVm.standard", + "carrier_name": "gartest2Carrier", + "service_name": "Standard" + }, + { + "id": "mBbc7RwNE6CPvx22XiDZ50kdsC2fLNEF.standard", + "carrier_name": "CSS Carrier", + "service_name": "Standard" + }, + { + "id": "courrierplus-494.standard", + "carrier_name": "Courrier Plus", + "service_name": "Standard" + }, + { + "id": "loomis-express.express-0900", + "carrier_name": "Loomis", + "service_name": "Express 9:00" + }, + { + "id": "loomis-express.express-1200", + "carrier_name": "Loomis", + "service_name": "Express 12:00" + }, + { + "id": "loomis-express.express-1800", + "carrier_name": "Loomis", + "service_name": "Express 18:00" + }, + { + "id": "loomis-express.ground", + "carrier_name": "Loomis", + "service_name": "Ground" + }, + { + "id": "purolatorfreight.standard", + "carrier_name": "Purolator Freight", + "service_name": "Standard" + }, + { + "id": "csatransportation-191.standard", + "carrier_name": "CSA Transportation", + "service_name": "Standard" + }, + { + "id": "XiX2uCQlUYTCn4Mi5z3APT4Mi2Xn2x3a.standard", + "carrier_name": "New GLS Freight", + "service_name": "Standard" + }, + { + "id": "fastfrate.express", + "carrier_name": "Fastfrate", + "service_name": "Express" + }, + { + "id": "fastfrate.standard", + "carrier_name": "Fastfrate", + "service_name": "Standard" + }, + { + "id": "martinroytransport-310.standard", + "carrier_name": "Martin Roy Transport", + "service_name": "Standard" + }, + { + "id": "reddaway.guaranteed-3pm", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Delivery Before 3:30 PM" + }, + { + "id": "reddaway.guaranteed-9am", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Delivery Before 9 AM" + }, + { + "id": "reddaway.guaranteed-noon", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Delivery Before 12:00 PM (noon)" + }, + { + "id": "reddaway.guaranteed-weekend", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Weekend" + }, + { + "id": "reddaway.guarenteed-9am", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Delivery Before 9 AM" + }, + { + "id": "reddaway.guarenteed-noon", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Delivery Before 12:00 PM (noon)" + }, + { + "id": "reddaway.interline", + "carrier_name": "Reddaway", + "service_name": "Interline Delivery" + }, + { + "id": "reddaway.multi-hour-window", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Window - Multi-Hour Window" + }, + { + "id": "reddaway.regional-delivery", + "carrier_name": "Reddaway", + "service_name": "Regional Delivery" + }, + { + "id": "reddaway.single-hour-window", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Window - Single-hour Window" + }, + { + "id": "reddaway.single-or-multi-day", + "carrier_name": "Reddaway", + "service_name": "Guaranteed Window - Single or Multi-day Window" + }, + { + "id": "reddaway.standard", + "carrier_name": "Reddaway", + "service_name": "Standard" + }, + { + "id": "xpo.standard", + "carrier_name": "XPO", + "service_name": "Standard" + }, + { + "id": "yrcfreight-330.standard", + "carrier_name": "YRC Freight", + "service_name": "Standard" + }, + { + "id": "amatransinc-251.standard", + "carrier_name": "AMA Trans Inc", + "service_name": "Standard" + }, + { + "id": "canadapost.domestic", + "carrier_name": "Canada Post", + "service_name": "Domestic" + }, + { + "id": "canadapost.expedited-parcel", + "carrier_name": "Canada Post", + "service_name": "Expedited Parcel" + }, + { + "id": "canadapost.expedited-parcel-usa", + "carrier_name": "Canada Post", + "service_name": "Expedited Parcel USA" + }, + { + "id": "canadapost.international", + "carrier_name": "Canada Post", + "service_name": "International" + }, + { + "id": "canadapost.international-parcel-air", + "carrier_name": "Canada Post", + "service_name": "International Parcel Air" + }, + { + "id": "canadapost.international-parcel-surface", + "carrier_name": "Canada Post", + "service_name": "International Parcel Surface" + }, + { + "id": "canadapost.priority", + "carrier_name": "Canada Post", + "service_name": "Priority" + }, + { + "id": "canadapost.priority-ww-envelope-international", + "carrier_name": "Canada Post", + "service_name": "Priority Worldwide envelope INT’L" + }, + { + "id": "canadapost.priority-ww-envelope-usa", + "carrier_name": "Canada Post", + "service_name": "Priority Worldwide envelope USA" + }, + { + "id": "canadapost.priority-ww-pak-international", + "carrier_name": "Canada Post", + "service_name": "Priority Worldwide pak INT’L" + }, + { + "id": "canadapost.priority-ww-pak-usa", + "carrier_name": "Canada Post", + "service_name": "Priority Worldwide pak USA" + }, + { + "id": "canadapost.priority-ww-parcel-international", + "carrier_name": "Canada Post", + "service_name": "Priority Worldwide parcel INT’L" + }, + { + "id": "canadapost.priority-ww-parcel-usa", + "carrier_name": "Canada Post", + "service_name": "Priority Worldwide parcel USA" + }, + { + "id": "canadapost.regular-parcel", + "carrier_name": "Canada Post", + "service_name": "Regular Parcel" + }, + { + "id": "canadapost.small-packet-international-air", + "carrier_name": "Canada Post", + "service_name": "Small Packet International Air" + }, + { + "id": "canadapost.small-packet-international-surface", + "carrier_name": "Canada Post", + "service_name": "Small Packet International Surface" + }, + { + "id": "canadapost.small-packet-usa-air", + "carrier_name": "Canada Post", + "service_name": "Small Packet USA Air" + }, + { + "id": "canadapost.tracked-packet-international", + "carrier_name": "Canada Post", + "service_name": "Tracked Packet - International" + }, + { + "id": "canadapost.tracked-packet-usa", + "carrier_name": "Canada Post", + "service_name": "Tracked Packet USA" + }, + { + "id": "canadapost.xpresspost", + "carrier_name": "Canada Post", + "service_name": "Xpresspost" + }, + { + "id": "canadapost.xpresspost-international", + "carrier_name": "Canada Post", + "service_name": "Xpresspost International" + }, + { + "id": "canadapost.xpresspost-usa", + "carrier_name": "Canada Post", + "service_name": "Xpresspost USA" + }, + { + "id": "canpar.ground", + "carrier_name": "Canpar", + "service_name": "Ground" + }, + { + "id": "canpar.international", + "carrier_name": "Canpar", + "service_name": "International" + }, + { + "id": "canpar.overnight", + "carrier_name": "Canpar", + "service_name": "Overnight" + }, + { + "id": "canpar.overnight-letter", + "carrier_name": "Canpar", + "service_name": "Overnight Letter" + }, + { + "id": "canpar.overnight-pak", + "carrier_name": "Canpar", + "service_name": "Overnight Pak" + }, + { + "id": "canpar.select", + "carrier_name": "Canpar", + "service_name": "Select" + }, + { + "id": "canpar.select-letter", + "carrier_name": "Canpar", + "service_name": "Select Letter" + }, + { + "id": "canpar.select-pak", + "carrier_name": "Canpar", + "service_name": "Select Pak" + }, + { + "id": "canpar.select-usa", + "carrier_name": "Canpar", + "service_name": "Select U.S.A." + }, + { + "id": "canpar.usa", + "carrier_name": "Canpar", + "service_name": "U.S.A." + }, + { + "id": "canpar.usa-Letter", + "carrier_name": "Canpar", + "service_name": "U.S.A. Letter" + }, + { + "id": "canpar.usa-pak", + "carrier_name": "Canpar", + "service_name": "U.S.A. Pak" + }, + { + "id": "gls.ground", + "carrier_name": "GLS", + "service_name": "Ground" + }, + { + "id": "QzGk6Gz9BPcw0v7zsJcIaTAAXd6X7732.standard", + "carrier_name": "BinduTestCarrier2", + "service_name": "Standard" + }, + { + "id": "w2oXoP88bl44DA3Y4hft8dZoLFDp3KFX.standard", + "carrier_name": "Carrier Sonu", + "service_name": "Standard" + }, + { + "id": "e5966ZlRusUUM61sXUM1QQx0Td17iIan.standard", + "carrier_name": "Dolphin Carrier", + "service_name": "Standard" + }, + { + "id": "polarisweb.standard", + "carrier_name": "Polaris FPP", + "service_name": "Pallet Program" + }, + { + "id": "purolatorcourier.express", + "carrier_name": "Purolator", + "service_name": "Express" + }, + { + "id": "purolatorcourier.express-box", + "carrier_name": "Purolator", + "service_name": "Express Box" + }, + { + "id": "purolatorcourier.express-box-international", + "carrier_name": "Purolator", + "service_name": "Express Box International" + }, + { + "id": "purolatorcourier.express-box1030am", + "carrier_name": "Purolator", + "service_name": "Express Box 10:30 AM" + }, + { + "id": "purolatorcourier.express-box9am", + "carrier_name": "Purolator", + "service_name": "Express Box 9 AM" + }, + { + "id": "purolatorcourier.express-boxUS", + "carrier_name": "Purolator", + "service_name": "Express Box U.S." + }, + { + "id": "purolatorcourier.express-envelope", + "carrier_name": "Purolator", + "service_name": "Express Envelope" + }, + { + "id": "purolatorcourier.express-envelope-international", + "carrier_name": "Purolator", + "service_name": "Express Envelope International" + }, + { + "id": "purolatorcourier.express-envelope-us", + "carrier_name": "Purolator", + "service_name": "Express Envelope U.S." + }, + { + "id": "purolatorcourier.express-envelope1030am", + "carrier_name": "Purolator", + "service_name": "Express Envelope 10:30 AM" + }, + { + "id": "purolatorcourier.express-envelope9am", + "carrier_name": "Purolator", + "service_name": "Express Envelope 9 AM" + }, + { + "id": "purolatorcourier.express-international", + "carrier_name": "Purolator", + "service_name": "Express International" + }, + { + "id": "purolatorcourier.express-pack", + "carrier_name": "Purolator", + "service_name": "Express Pack" + }, + { + "id": "purolatorcourier.express-pack-international", + "carrier_name": "Purolator", + "service_name": "Express Pack International" + }, + { + "id": "purolatorcourier.express-pack-us", + "carrier_name": "Purolator", + "service_name": "Express Pack U.S." + }, + { + "id": "purolatorcourier.express-pack1030am", + "carrier_name": "Purolator", + "service_name": "Express Pack 10:30 AM" + }, + { + "id": "purolatorcourier.express-pack9am", + "carrier_name": "Purolator", + "service_name": "Express Pack 9 AM" + }, + { + "id": "purolatorcourier.express-us", + "carrier_name": "Purolator", + "service_name": "Express U.S." + }, + { + "id": "purolatorcourier.express-us-1030am", + "carrier_name": "Purolator", + "service_name": "Express U.S. 10:30 AM" + }, + { + "id": "purolatorcourier.express-us-9am", + "carrier_name": "Purolator", + "service_name": "Express U.S. 9 AM" + }, + { + "id": "purolatorcourier.express-us-box1030AM", + "carrier_name": "Purolator", + "service_name": "Express U.S. Box 10:30 AM" + }, + { + "id": "purolatorcourier.express-us-box9AM", + "carrier_name": "Purolator", + "service_name": "Express U.S. Box 9 AM" + }, + { + "id": "purolatorcourier.express-us-envelope1030am", + "carrier_name": "Purolator", + "service_name": "Express U.S. Envelope 10:30 AM" + }, + { + "id": "purolatorcourier.express-us-envelope9am", + "carrier_name": "Purolator", + "service_name": "Express U.S. Envelope 9 AM" + }, + { + "id": "purolatorcourier.express-us-pack1030am", + "carrier_name": "Purolator", + "service_name": "Express U.S. Pack 10:30 AM" + }, + { + "id": "purolatorcourier.express-us-pack9am", + "carrier_name": "Purolator", + "service_name": "Express U.S. Pack 9 AM" + }, + { + "id": "purolatorcourier.express1030AM", + "carrier_name": "Purolator", + "service_name": "Express 10:30 AM" + }, + { + "id": "purolatorcourier.express9AM", + "carrier_name": "Purolator", + "service_name": "Express 9 AM" + }, + { + "id": "purolatorcourier.ground", + "carrier_name": "Purolator", + "service_name": "Ground" + }, + { + "id": "purolatorcourier.ground-us", + "carrier_name": "Purolator", + "service_name": "Ground U.S." + }, + { + "id": "purolatorcourier.standard", + "carrier_name": "Purolator", + "service_name": "Standard" + } + ] +} diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py new file mode 100644 index 0000000..b191558 --- /dev/null +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py @@ -0,0 +1,252 @@ +"""Karrio Freightcom Rest rate API implementation.""" + + +import karrio.schemas.freightcom_rest.rate_request as freightcom_rest_req +import karrio.schemas.freightcom_rest.rate_response as freightcom_rest_res + +import typing +import karrio.lib as lib +import karrio.core.units as units +import karrio.core.models as models +import karrio.providers.freightcom_rest.error as error +import karrio.providers.freightcom_rest.utils as provider_utils +import karrio.providers.freightcom_rest.units as provider_units + +import datetime + +def parse_rate_response( + _response: lib.Deserializable[dict], + settings: provider_utils.Settings, +) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]: + response = _response.deserialize() + + messages = error.parse_error_response(response, settings) + + # Extract rate objects from the response - adjust based on carrier API structure + + # For JSON APIs, find the path to rate objects + rate_objects = response.get("rates", []) if hasattr(response, 'get') else [] + rates = [_extract_details(rate, settings) for rate in rate_objects] + + + return rates, messages + + +def _extract_details( + data: dict, + settings: provider_utils.Settings, +) -> models.RateDetails: + """ + Extract rate details from carrier response data + + data: The carrier-specific rate data structure + settings: The carrier connection settings + + Returns a RateDetails object with extracted rate information + """ + # Convert the carrier data to a proper object for easy attribute access + + # For JSON APIs, convert dict to proper response object + rate = lib.to_object(freightcom_rest_res.RateType, data) + + # Now access data through the object attributes + service = provider_units.ShippingService.map( + rate.service_id, + ) + service_name = service.name_or_key if service else "" + + courier = provider_units.ShippingCourier.find(rate.service_id) + rate_provider = courier.name_or_key if courier else "" + + total_obj = rate.total if hasattr(rate, 'total') else None + + total = float(int(total_obj.value) / 100) if hasattr(total_obj, 'value') and total_obj.value else 0.0 + currency = total_obj.currency if hasattr(total_obj, 'currency') else "USD" + transit_days = int(rate.transit_time_days) if hasattr(rate, 'transit_time_days') and rate.transit_time_days else 0 + + charges = [ + ("Base charge", rate.base.value, rate.base.currency), + *((surcharge.type, surcharge.amount.value, surcharge.amount.currency) for surcharge in rate.surcharges), + *((tax.type, tax.amount.value, tax.amount.currency) for tax in rate.taxes), + ] + + + return models.RateDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + service=service_name, + total_charge=lib.to_money(total), + currency=currency, + transit_days=transit_days, + extra_charges=[ + models.ChargeDetails( + name=name, + currency=currency, + amount=lib.to_money(int(amount) / 100), + ) + for name, amount, currency in charges + if charges + ], + meta=dict( + service_name=service_name, + rate_provider=rate_provider, + # Add any other useful metadata from the carrier response + ), + ) + + +def rate_request( + payload: models.RateRequest, + settings: provider_utils.Settings, +) -> lib.Serializable: + """ + Create a rate request for the carrier API + + payload: The standardized RateRequest from karrio + settings: The carrier connection settings + + Returns a Serializable object that can be sent to the carrier API + """ + # Convert karrio models to carrier-specific format + shipper = lib.to_address(payload.shipper) + recipient = lib.to_address(payload.recipient) + packages = lib.to_packages(payload.parcels) + services = lib.to_services(payload.services, provider_units.ShippingService) + options = lib.to_shipping_options( + payload.options, + package_options=packages.options, + initializer=provider_units.shipping_options_initializer, + ) + + # Create the carrier-specific request object + + packaging_type = provider_units.PackagingType.map(packages.package_type or "small_box").value + ship_datetime = lib.to_next_business_datetime( + options.shipping_date.state or datetime.datetime.now(), + current_format="%Y-%m-%dT%H:%M", + ) + + request = freightcom_rest_req.RateRequestType( + services=[provider_units.ShippingService.map(service).value_or_key for service in payload.services], + excluded_services=[], + details=freightcom_rest_req.DetailsType( + origin=freightcom_rest_req.DestinationType( + name=shipper.company_name or shipper.person_name, + address=freightcom_rest_req.AddressType( + address_line_1=shipper.address_line1, + address_line_2=shipper.address_line2, + city=shipper.city, + region=shipper.state_code, + country=shipper.country_code, + postal_code=shipper.postal_code, + ), + residential=shipper.residential is True, + contact_name=shipper.person_name if shipper.company_name else "", + phone_number=freightcom_rest_req.PhoneNumberType( + number=shipper.phone_number, + ) if shipper.phone_number else None, + email_addresses=lib.join(shipper.email), + ), + destination=freightcom_rest_req.DestinationType( + name=recipient.company_name or recipient.person_name, + address=freightcom_rest_req.AddressType( + address_line_1=recipient.address_line1, + address_line_2=recipient.address_line2, + city=recipient.city, + region=recipient.state_code, + country=recipient.country_code, + postal_code=recipient.postal_code, + ), + residential=recipient.residential is True, + contact_name=recipient.person_name, + phone_number=freightcom_rest_req.PhoneNumberType( + number=recipient.phone_number + ) if recipient.phone_number else None, + email_addresses=lib.join(recipient.email), + ready_at=freightcom_rest_req.ReadyType( + hour=ship_datetime.hour, + minute=0 + ), + ready_until=freightcom_rest_req.ReadyType( + hour=17, + minute=0 + ), + receives_email_updates=options.email_notification.state, + signature_requirement="required" if options.signature_confirmation.state else "not-required" + ), + expected_ship_date=freightcom_rest_req.ExpectedShipDateType( + year=ship_datetime.year, + month=ship_datetime.month, + day=ship_datetime.day, + ), + packaging_type=packaging_type, + packaging_properties=( + freightcom_rest_req.PackagingPropertiesType( + pallet_type="ltl" if packaging_type == "pallet" else None, + has_stackable_pallets=options.stackable.state if packaging_type == "pallet" else None, + dangerous_goods=options.dangerous_goods.state, + dangerous_goods_details=freightcom_rest_req.DangerousGoodsDetailsType( + packaging_group=options.dangerous_goods_group.state, + goods_class=options.dangerous_goods_class.state, + ) if options.dangerous_goods.state else None, + pallets=[ + freightcom_rest_req.PalletType( + measurements=freightcom_rest_req.PackageMeasurementsType( + weight=freightcom_rest_req.WeightType( + unit="kg", + value=parcel.weight.KG + ), + cuboid=freightcom_rest_req.CuboidType( + unit="cm", + l=parcel.length.CM, + w=parcel.width.CM, + h=parcel.height.CM + ) + ), + description=parcel.description, + freight_class=options.freight_class.state, + ) for parcel in packages + ] if packaging_type == "pallet" else [], + packages=[ + freightcom_rest_req.PackageType( + measurements=freightcom_rest_req.PackageMeasurementsType( + weight=freightcom_rest_req.WeightType( + unit="kg", + value=parcel.weight.KG + ), + cuboid=freightcom_rest_req.CuboidType( + unit="cm", + l=parcel.length.CM, + w=parcel.width.CM, + h=parcel.height.CM + ) + ), + description=parcel.description, + ) for parcel in packages + ] if packaging_type == "package" else [], + courierpaks=[ + freightcom_rest_req.CourierpakType( + measurements=freightcom_rest_req.CourierpakMeasurementsType( + weight=freightcom_rest_req.WeightType( + unit="kg", + value=parcel.weight.KG + ), + ), + description=parcel.description, + ) for parcel in packages + ] if packaging_type == "courier-pak" else [], + insurance=freightcom_rest_req.InsuranceType( + type='carrier', + total_cost=freightcom_rest_req.TotalCostType( + currency=options.currency.state or "CAD", + value=lib.to_int(options.insurance.state) + ) + ) if options.insurance.state else None, + pallet_service_details=freightcom_rest_req.PalletServiceDetailsType() if packaging_type == "pallet" else None, + ) + ), + reference_codes=[payload.reference] if any(payload.reference or "") else [] + ) + ) + return lib.Serializable(request, lib.to_dict) + diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/__init__.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/__init__.py new file mode 100644 index 0000000..dcc887e --- /dev/null +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/__init__.py @@ -0,0 +1,9 @@ + +from karrio.providers.freightcom_rest.shipment.create import ( + parse_shipment_response, + shipment_request, +) +from karrio.providers.freightcom_rest.shipment.cancel import ( + parse_shipment_cancel_response, + shipment_cancel_request, +) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/cancel.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/cancel.py new file mode 100644 index 0000000..bdd16f0 --- /dev/null +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/cancel.py @@ -0,0 +1,75 @@ +"""Karrio Freightcom Rest shipment cancellation API implementation.""" +import typing +import karrio.lib as lib +import karrio.core.models as models +import karrio.providers.freightcom_rest.error as error +import karrio.providers.freightcom_rest.utils as provider_utils +import karrio.providers.freightcom_rest.units as provider_units + + +def parse_shipment_cancel_response( + _response: lib.Deserializable[dict], + settings: provider_utils.Settings, +) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]: + """ + Parse shipment cancellation response from carrier API + + _response: The carrier response to deserialize + settings: The carrier connection settings + + Returns a tuple with (ConfirmationDetails, List[Message]) + """ + response = _response.deserialize() + messages = error.parse_error_response(response, settings) + + # Extract success state from the response + # success = _extract_cancellation_status(response) + success = not any(messages) + + # Create confirmation details if successful + confirmation = ( + models.ConfirmationDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + operation="Cancel Shipment", + success=success, + ) if success else None + ) + + return confirmation, messages + +# +# def _extract_cancellation_status( +# response: dict +# ) -> bool: +# """ +# Extract cancellation success status from the carrier response +# +# response: The deserialized carrier response +# +# Returns True if cancellation was successful, False otherwise +# """ +# +# # Example implementation for JSON response: +# # return response.get("success", False) +# +# # For development, always return success +# return True + + + +def shipment_cancel_request( + payload: models.ShipmentCancelRequest, + settings: provider_utils.Settings, +) -> lib.Serializable: + """ + Create a shipment cancellation request for the carrier API + + payload: The standardized ShipmentCancelRequest from karrio + settings: The carrier connection settings + + Returns a Serializable object that can be sent to the carrier API + """ + + return lib.Serializable(payload.shipment_identifier) + diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py new file mode 100644 index 0000000..7405880 --- /dev/null +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -0,0 +1,387 @@ +"""Karrio Freightcom Rest shipment API implementation.""" + +# IMPLEMENTATION INSTRUCTIONS: +# 1. Uncomment the imports when the schema types are generated +# 2. Import the specific request and response types you need +# 3. Create a request instance with the appropriate request type +# 4. Extract shipment details from the response +# +# NOTE: JSON schema types are generated with "Type" suffix (e.g., ShipmentRequestType), +# while XML schema types don't have this suffix (e.g., ShipmentRequest). + +import karrio.schemas.freightcom_rest.shipment_request as freightcom_rest_req +import karrio.schemas.freightcom_rest.shipment_response as freightcom_rest_res + +import typing +import karrio.lib as lib +import karrio.core.units as units +import karrio.core.models as models +import karrio.providers.freightcom_rest.error as error +import karrio.providers.freightcom_rest.utils as provider_utils +import karrio.providers.freightcom_rest.units as provider_units + +import datetime +import uuid + + +def parse_shipment_response( + _response: lib.Deserializable[dict], + settings: provider_utils.Settings, +) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]: + response = _response.deserialize() + messages = error.parse_error_response(response, settings) + + # Check if we have valid shipment data + + has_shipment = "shipment" in response if hasattr(response, 'get') else False + + shipment = _extract_details(response, settings, ctx=_response._ctx) if has_shipment else None + + return shipment, messages + + +def _extract_details( + data: dict, + settings: provider_utils.Settings, + ctx: dict, +) -> models.ShipmentDetails: + """ + Extract shipment details from carrier response data + + data: The carrier-specific shipment data structure + settings: The carrier connection settings + + Returns a ShipmentDetails object with extracted shipment information + """ + # Convert the carrier data to a proper object for easy attribute access + + # For JSON APIs, convert dict to proper response object + response_obj = lib.to_object(freightcom_rest_res.ShipmentResponseType, data) + + # Access the shipment data + shipment = response_obj.shipment if hasattr(response_obj, 'shipment') else None + + if shipment: + # Extract tracking info + tracking_number = shipment.primary_tracking_number if hasattr(shipment, 'primary_tracking_number') else "" + shipment_id = shipment.id if hasattr(shipment, 'id') else "" + + # Extract label info + label_format = "ZPL" if ctx.get("label_type") == "ZPL" else "PDF" + + label_url = [_.url for _ in shipment.labels if + _.format == label_format.lower() and _.size == "a6" and not _.padded] + label_base64 = provider_utils.download_document_to_base64(label_url[0]) if label_url else "" + + # Extract optional invoice + customers_invoice_url = shipment.customs_invoice_url if hasattr(shipment, 'customs_invoice_url') else "" + invoice_base64 = provider_utils.download_document_to_base64(shipment.customs_invoice_url) if customers_invoice_url else "" + + # Extract service code for metadata + # service_code = shipment.serviceCode if hasattr(shipment, 'serviceCode') else "" + + tracking_numbers = ( + ([shipment.primary_tracking_number] if hasattr(shipment, "primary_tracking_number") else []) + + [ + tn for tn in shipment.tracking_numbers + if hasattr(shipment, "primary_tracking_number") and tn != shipment.primary_tracking_number + ] + ) + + rate = shipment.rate + service = provider_units.ShippingService.map( + rate.service_id, + ) + service_name = service.name_or_key + courier = provider_units.ShippingCourier.find(rate.service_id) + + rate_provider = courier.name_or_key + carrier_tracking_link = shipment.tracking_url if hasattr(shipment, 'tracking_url') else "" + + freightcom_service_id = rate.service_id if hasattr(rate, 'service_id') else "" + freightcom_unique_id = shipment.unique_id if hasattr(shipment, 'unique_id') else "" + + else: + tracking_number = "" + shipment_id = "" + label_format = "PDF" + label_base64 = "" + invoice_base64 = "" + # service_code = "" + + # added + tracking_numbers = "" + service_name = "" + rate_provider = "" + carrier_tracking_link = "" + freightcom_service_id = "" + freightcom_unique_id = "" + + documents = models.Documents( + label=label_base64, + ) + + # Add invoice if present + if invoice_base64: + documents.invoice = invoice_base64 + + return models.ShipmentDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + tracking_number=tracking_number, + shipment_identifier=shipment_id, + label_type=label_format, + docs=documents, + meta=dict( + # service_code=service_code, + carrier_tracking_link=carrier_tracking_link, + tracking_numbers=tracking_numbers, + rate_provider=rate_provider, + service_name=service_name, + freightcom_service_id=freightcom_service_id, + freightcom_unique_id=freightcom_unique_id, + freightcom_shipment_identifier=shipment_id + ), + ) + + + +def shipment_request( + payload: models.ShipmentRequest, + settings: provider_utils.Settings, +) -> lib.Serializable: + """ + Create a shipment request for the carrier API + + payload: The standardized ShipmentRequest from karrio + settings: The carrier connection settings + + Returns a Serializable object that can be sent to the carrier API + """ + # Convert karrio models to carrier-specific format + shipper = lib.to_address(payload.shipper) + recipient = lib.to_address(payload.recipient) + packages = lib.to_packages(payload.parcels) + service = provider_units.ShippingService.map(payload.service).value_or_key + options = lib.to_shipping_options( + payload.options, + package_options=packages.options, + initializer=provider_units.shipping_options_initializer, + ) + + # Create the carrier-specific request object + packaging_type = provider_units.PackagingType.map( + packages.package_type or "small_box" + ).value + + ship_datetime = lib.to_next_business_datetime( + options.shipping_date.state or datetime.datetime.now(), + current_format="%Y-%m-%dT%H:%M", + ) + + is_intl = shipper.country_code != recipient.country_code + customs = lib.to_customs_info( + payload.customs, + shipper=payload.shipper, + recipient=payload.recipient, + weight_unit=packages.weight_unit, + default_to=( + models.Customs( + commodities=( + packages.items + if any(packages.items) + else [ + models.Commodity( + quantity=1, + sku=f"000{index}", + weight=pkg.weight.value, + weight_unit=pkg.weight_unit.value, + description=pkg.parcel.content, + ) + for index, pkg in enumerate(packages, start=1) + ] + ) + ) + if is_intl + else None + ), + ) + + payment_method_id = settings.payment_method + + if not payment_method_id: + raise Exception("No payment method found need to be set in config") + + request = freightcom_rest_req.ShipmentRequestType( + unique_id=str(uuid.uuid4()), + payment_method_id=payment_method_id, + service_id=provider_units.ShippingService.map(payload.service).value_or_key, + details=freightcom_rest_req.ShipmentRequestDetailsType( + origin=freightcom_rest_req.DestinationType( + name=shipper.company_name or shipper.person_name, + address=freightcom_rest_req.AddressType( + address_line_1=shipper.address_line1, + address_line_2=shipper.address_line2, + city=shipper.city, + region=shipper.state_code, + country=shipper.country_code, + postal_code=shipper.postal_code, + ), + residential=shipper.residential is True, + contact_name=shipper.person_name if shipper.company_name else "", + phone_number=freightcom_rest_req.NumberType( + number=shipper.phone_number + ) if shipper.phone_number else None, + email_addresses=[shipper.email] if shipper.email else [], + ), + destination=freightcom_rest_req.DestinationType( + name=recipient.company_name or recipient.person_name, + address=freightcom_rest_req.AddressType( + address_line_1=recipient.address_line1, + address_line_2=recipient.address_line2, + city=recipient.city, + region=recipient.state_code, + country=recipient.country_code, + postal_code=recipient.postal_code, + ), + residential=recipient.residential is True, + contact_name=recipient.person_name, + phone_number=freightcom_rest_req.NumberType( + number=recipient.phone_number + ) if recipient.phone_number else None, + email_addresses=[recipient.email] if recipient.email else [], + ready_at=freightcom_rest_req.ReadyType( + hour=ship_datetime.hour, + minute=0 + ), + ready_until=freightcom_rest_req.ReadyType( + hour=17, + minute=0 + ), + receives_email_updates=options.email_notification.state, + signature_requirement="required" if options.signature_confirmation.state else "not-required" + ), + expected_ship_date=freightcom_rest_req.DateType( + year=ship_datetime.year, + month=ship_datetime.month, + day=ship_datetime.day, + ), + packaging_type=packaging_type, + packaging_properties=freightcom_rest_req.PackagingPropertiesType( + pallet_type="ltl" if packaging_type == "pallet" else None, + has_stackable_pallets=options.stackable.state if packaging_type == "pallet" else None, + dangerous_goods=options.dangerous_goods.state, + dangerous_goods_details=freightcom_rest_req.DangerousGoodsDetailsType( + packaging_group=options.dangerous_goods_group.state, + goods_class=options.dangerous_goods_class.state, + ) if options.dangerous_goods.state else None, + pallets=[ + freightcom_rest_req.PalletType( + measurements=freightcom_rest_req.PackageMeasurementsType( + weight=freightcom_rest_req.WeightType( + unit="kg", + value=parcel.weight.KG + ), + cuboid=freightcom_rest_req.CuboidType( + unit="cm", + l=parcel.length.CM, + w=parcel.width.CM, + h=parcel.height.CM + ) + ), + description=parcel.description or "N/A", + freight_class=options.freight_class.state, + ) for parcel in packages + ] if packaging_type == "pallet" else [], + packages=[ + freightcom_rest_req.PackageType( + measurements=freightcom_rest_req.PackageMeasurementsType( + weight=freightcom_rest_req.WeightType( + unit="kg", + value=parcel.weight.KG + ), + cuboid=freightcom_rest_req.CuboidType( + unit="cm", + l=parcel.length.CM, + w=parcel.width.CM, + h=parcel.height.CM + ) + ), + description=parcel.description or "N/A", + ) for parcel in packages + ] if packaging_type == "package" else [], + courierpaks=[ + freightcom_rest_req.CourierpakType( + measurements=freightcom_rest_req.CourierpakMeasurementsType( + weight=freightcom_rest_req.WeightType( + unit="kg", + value=parcel.weight.KG + ), + ), + description=parcel.description or "N/A", + ) for parcel in packages + ] if packaging_type == "courier-pak" else [], + ), + reference_codes=[payload.reference] if payload.reference else [] + ), + customs_invoice=( + freightcom_rest_req.CustomsInvoiceType( + source="details", + broker=freightcom_rest_req.BrokerType( + use_carrier=True, + ), + details=freightcom_rest_req.CustomsInvoiceDetailsType( + products=[ + freightcom_rest_req.ProductType( + product_name=item.description, + weight=freightcom_rest_req.WeightType( + unit="kg" if item.weight_unit.upper() == "KG" else "lb", + value=lib.to_decimal(item.weight) + ), + hs_code=item.hs_code, + country_of_origin=item.origin_country, + num_units=item.quantity, + unit_price=freightcom_rest_req.TotalCostType( + currency=item.value_currency, + value=str(item.value_amount) + ), + description=item.description + ) for item in customs.commodities + ], + tax_recipient=freightcom_rest_req.TaxRecipientType( + type=provider_units.PaymentType.map( + customs.duty.paid_by + ).value + or "shipper", + name=customs.duty_billing_address.company_name or customs.duty.person_name, + address=freightcom_rest_req.AddressType( + address_line_1=customs.duty_billing_address.address_line1, + address_line_2=customs.duty_billing_address.address_line2, + city=customs.duty_billing_address.city, + region=customs.duty_billing_address.state_code, + country=customs.duty_billing_address.country_code, + postal_code=customs.duty_billing_address.postal_code, + ), + phone_number=freightcom_rest_req.NumberType( + number=customs.duty_billing_address.phone_number + ), + reason_for_export=provider_units.CustomsContentType.map( + customs.content_type + ).value, + ) + ) + ) + if customs and customs.commodities + else None + ), + #TODO: validate if we need to do pickup in the ship request + # pickup_details=freightcom.PickupDetailsType( + # + # ) + ) + + return lib.Serializable( + request, + lib.to_dict, + dict(label_type=payload.label_type or "PDF") + ) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py new file mode 100644 index 0000000..cc21ec2 --- /dev/null +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py @@ -0,0 +1,177 @@ +import pathlib +import typing + +import karrio.lib as lib +import karrio.core.units as units + +METADATA_JSON = lib.load_json(pathlib.Path(__file__).resolve().parent / "metadata.json") +FREIGHTCOM_CARRIER_METADATA = [_ for _ in METADATA_JSON["PROD_SERVICES"] + METADATA_JSON["DEV_SERVICES"]] + + +KARRIO_CARRIER_MAPPING = { + "Freightcom": "freightcom", + "ups_courier": "ups", + "canada_post": "canadapost", + 'fed_ex_courier': "fedex", + 'fed_ex_express': "fedex", + 'fed_ex_freight': "fedex", + 'fed_ex_ground': "fedex", + "dhl_canada": "dhl_express", + "dhl_e_commerce": "dhl_express", +} + +class PackagingType(lib.StrEnum): + """ Carrier specific packaging type """ + # TODO: review types + freightcom_pallet = "pallet" + freightcom_drum = "Drum" + freightcom_boxes = "Boxes" + freightcom_rolls = "Rolls" + freightcom_pipes_tubes = "Pipes/Tubes" + freightcom_bales = "Bales" + freightcom_bags = "Bags" + freightcom_cylinder = "Cylinder" + freightcom_pails = "Pails" + freightcom_reels = "Reels" + + freightcom_envelope = "envelope" + freightcom_courier = "courier-pak" + freightcom_pak = "courier-pak" + freightcom_package = "package" + + """ Unified Packaging type mapping """ + envelope = freightcom_envelope + pak = freightcom_pak + tube = freightcom_pipes_tubes + pallet = freightcom_pallet + small_box = freightcom_package + medium_box = freightcom_package + your_packaging = freightcom_package + + +class PaymentType(lib.StrEnum): # TODO:: retrieve the complete list of payment types + sender = "shipper" + recipient = "receiver" + third_party = "other" + +class CustomsContentType(lib.StrEnum): + sale = "commercially-sold-goods" + gift = "gift" + sample = "commercial-sample" + repair = "repair-warranty" + return_merchandise = "return-shipment" + other = "other" + + """ Unified Content type mapping """ + documents = other + merchandise = sale + + +class ShippingOption(lib.Enum): + freightcom_signature_required = lib.OptionEnum("signatureRequired", bool) + freightcom_saturday_pickup_required = lib.OptionEnum("saturdayPickupRequired", bool) + freightcom_homeland_security = lib.OptionEnum("homelandSecurity", bool) + freightcom_exhibition_convention_site = lib.OptionEnum( + "exhibitionConventionSite", bool + ) + freightcom_military_base_delivery = lib.OptionEnum("militaryBaseDelivery", bool) + freightcom_customs_in_bond_freight = lib.OptionEnum("customsIn_bondFreight", bool) + freightcom_limited_access = lib.OptionEnum("limitedAccess", bool) + freightcom_excess_length = lib.OptionEnum("excessLength", bool) + freightcom_tailgate_pickup = lib.OptionEnum("tailgatePickup", bool) + freightcom_residential_pickup = lib.OptionEnum("residentialPickup", bool) + freightcom_cross_border_fee = lib.OptionEnum("crossBorderFee", bool) + freightcom_notify_recipient = lib.OptionEnum("notifyRecipient", bool) + freightcom_single_shipment = lib.OptionEnum("singleShipment", bool) + freightcom_tailgate_delivery = lib.OptionEnum("tailgateDelivery", bool) + freightcom_residential_delivery = lib.OptionEnum("residentialDelivery", bool) + freightcom_insurance_type = lib.OptionEnum("insuranceType", float) + freightcom_inside_delivery = lib.OptionEnum("insideDelivery", bool) + freightcom_is_saturday_service = lib.OptionEnum("isSaturdayService", bool) + freightcom_dangerous_goods_type = lib.OptionEnum("dangerousGoodsType", bool) + freightcom_stackable = lib.OptionEnum("stackable", bool) + freightcom_payment_method = lib.OptionEnum("payment_method", str) + + """ Unified Option type mapping """ + # saturday_delivery = freightcom_saturday_pickup_required + # signature_confirmation = freightcom_signature_required + + +def shipping_options_initializer( + options: dict, + package_options: units.ShippingOptions = None, +) -> units.ShippingOptions: + """ + Apply default values to the given options. + """ + + if package_options is not None: + options.update(package_options.content) + + def items_filter(key: str) -> bool: + return key in ShippingOption # type: ignore + + return units.ShippingOptions(options, ShippingOption, items_filter=items_filter) + + +def to_carrier_code(service: typing.Dict[str, str]) -> str: + _code = lib.to_snake_case(service['carrier_name']) + return KARRIO_CARRIER_MAPPING.get(_code, _code) + +def to_service_code(service: typing.Dict[str, str]) -> str: + return f"freightcom_{to_carrier_code(service)}_{lib.to_slug(service['service_name'])}" + +def find_courier(search: str): + courier: dict = next( + ( + item + for item in FREIGHTCOM_CARRIER_METADATA + if to_carrier_code(item) == search + or item['carrier_name'] == search + or item['id'] == search + ), + {}, + ) + + if courier: + return ShippingCourier.map(to_carrier_code(courier)) + + return ShippingCourier.map(search) + +def get_carrier_name(carrier_id: str) -> str: + return next( + ( + service['carrier_name'] + for service in METADATA_JSON["PROD_SERVICES"] + if service['id'].split('.')[0] == carrier_id + ), + None + ) + + +ShippingService = lib.StrEnum( + "ShippingService", + { + to_service_code(service): service['id'] + for service in FREIGHTCOM_CARRIER_METADATA + }, +) +ShippingCourier = lib.StrEnum( + "ShippingCourier", + { + to_carrier_code(service): service['carrier_name'] + for service in FREIGHTCOM_CARRIER_METADATA + }, +) + + +setattr(ShippingCourier, "find", find_courier) + +class TrackingStatus(lib.Enum): + on_hold = ["on_hold"] + delivered = ["delivered"] + in_transit = ["in_transit"] + delivery_failed = ["delivery_failed"] + delivery_delayed = ["delivery_delayed"] + out_for_delivery = ["out_for_delivery"] + ready_for_pickup = ["ready_for_pickup"] diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py new file mode 100644 index 0000000..a42909c --- /dev/null +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py @@ -0,0 +1,107 @@ + +import base64 +import datetime +import typing + +import karrio.lib as lib +import karrio.core as core +import karrio.core.errors as errors + +import math + +class Settings(core.Settings): + """Freightcom Rest connection settings.""" + + # Add carrier specific api connection properties here + api_key: str + + @property + def carrier_name(self): + return "freightcom_rest" + + @property + def server_url(self): + return ( + "https://customer-external-api.ssd-test.freightcom.com" + if self.test_mode + else "https://external-api.freightcom.com" + ) + + + # """uncomment the following code block to expose a carrier tracking url.""" + # @property + # def tracking_url(self): + # return "https://www.carrier.com/tracking?tracking-id={}" + + @property + def connection_config(self) -> lib.units.Options: + return lib.to_connection_config( + self.config or {}, + option_type=ConnectionConfig, + ) + + @property + def payment_method(self): + + if not self.connection_config.payment_method_type.state: + raise Exception(f"Payment method type not set") + cache_key = f"payment|{self.carrier_name}|{self.connection_config.payment_method_type.state}|{self.api_key}" + + payment = self.connection_cache.get(cache_key) or {} + payment_id = payment.get("id") + + if payment_id: + return payment_id + + self.connection_cache.set(cache_key, lambda: get_payment_id(self)) + new_auth = self.connection_cache.get(cache_key) + + return new_auth.get("id") + + +def download_document_to_base64(file_url: str) -> str: + return lib.request( + decoder=lambda b: base64.encodebytes(b).decode("utf-8"), + url=file_url, + ) + + +def ceil(value: typing.Optional[float]) -> typing.Optional[int]: + if value is None: + return None + return math.ceil(value) + +def get_payment_id(settings: Settings) -> dict: + + try: + from karrio.mappers.freightcom_rest.proxy import Proxy + + proxy = Proxy(settings) + response = proxy._get_payments_methods() + methods = response.deserialize() + + selected_method = next(( + method for method in methods + if settings.connection_config.payment_method_type.type.map( + method.get('type')).name == settings.connection_config.payment_method_type.state + ), None) + + + if not selected_method: + raise Exception(f"Payment method {settings.connection_config.payment_method_type.state} not found in API") + + return selected_method + + except Exception as e: + raise + + +class PaymentMethodType(lib.StrEnum): + net_terms = "net-terms" + credit_card = "credit-card" + +class ConnectionConfig(lib.Enum): + """Carrier specific connection configs""" + payment_method_type = lib.OptionEnum("payment_method_type", PaymentMethodType) + shipping_options = lib.OptionEnum("shipping_options", list) + shipping_services = lib.OptionEnum("shipping_services", list) diff --git a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/__init__.py b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/error_response.py b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/error_response.py new file mode 100644 index 0000000..d8cd6fd --- /dev/null +++ b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/error_response.py @@ -0,0 +1,14 @@ +import attr +import jstruct +import typing + + +@attr.s(auto_attribs=True) +class DataType: + services: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class ErrorResponseType: + message: typing.Optional[str] = None + data: typing.Optional[DataType] = jstruct.JStruct[DataType] diff --git a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/pickup_request.py b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/pickup_request.py new file mode 100644 index 0000000..cf01128 --- /dev/null +++ b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/pickup_request.py @@ -0,0 +1,46 @@ +import attr +import jstruct +import typing + + +@attr.s(auto_attribs=True) +class DateType: + year: typing.Optional[int] = None + month: typing.Optional[int] = None + day: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class ReadyType: + hour: typing.Optional[int] = None + minute: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class DispatchDetailsType: + date: typing.Optional[DateType] = jstruct.JStruct[DateType] + ready_at: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + ready_until: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + + +@attr.s(auto_attribs=True) +class ContactPhoneNumberType: + number: typing.Optional[str] = None + extension: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class PickupDetailsType: + pre_scheduled_pickup: typing.Optional[bool] = None + date: typing.Optional[DateType] = jstruct.JStruct[DateType] + ready_at: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + ready_until: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + pickup_location: typing.Optional[str] = None + contact_name: typing.Optional[str] = None + contact_phone_number: typing.Optional[ContactPhoneNumberType] = jstruct.JStruct[ContactPhoneNumberType] + + +@attr.s(auto_attribs=True) +class PickupRequestType: + pickup_details: typing.Optional[PickupDetailsType] = jstruct.JStruct[PickupDetailsType] + dispatch_details: typing.Optional[DispatchDetailsType] = jstruct.JStruct[DispatchDetailsType] diff --git a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_request.py b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_request.py new file mode 100644 index 0000000..c8c1b2a --- /dev/null +++ b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_request.py @@ -0,0 +1,173 @@ +import attr +import jstruct +import typing + + +@attr.s(auto_attribs=True) +class AddressType: + address_line_1: typing.Optional[str] = None + address_line_2: typing.Optional[str] = None + unit_number: typing.Optional[str] = None + city: typing.Optional[str] = None + region: typing.Optional[str] = None + country: typing.Optional[str] = None + postal_code: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class PhoneNumberType: + number: typing.Optional[str] = None + extension: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class ReadyType: + hour: typing.Optional[int] = None + minute: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class DestinationType: + name: typing.Optional[str] = None + address: typing.Optional[AddressType] = jstruct.JStruct[AddressType] + residential: typing.Optional[bool] = None + tailgate_required: typing.Optional[bool] = None + instructions: typing.Optional[str] = None + contact_name: typing.Optional[str] = None + phone_number: typing.Optional[PhoneNumberType] = jstruct.JStruct[PhoneNumberType] + email_addresses: typing.Optional[typing.List[str]] = None + receives_email_updates: typing.Optional[bool] = None + ready_at: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + ready_until: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + signature_requirement: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class ExpectedShipDateType: + year: typing.Optional[int] = None + month: typing.Optional[int] = None + day: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class WeightType: + unit: typing.Optional[str] = None + value: typing.Optional[float] = None + + +@attr.s(auto_attribs=True) +class CourierpakMeasurementsType: + weight: typing.Optional[WeightType] = jstruct.JStruct[WeightType] + + +@attr.s(auto_attribs=True) +class CourierpakType: + measurements: typing.Optional[CourierpakMeasurementsType] = jstruct.JStruct[CourierpakMeasurementsType] + description: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class DangerousGoodsDetailsType: + packaging_group: typing.Optional[str] = None + goods_class: typing.Optional[str] = None + description: typing.Optional[str] = None + united_nations_number: typing.Optional[str] = None + emergency_contact_name: typing.Optional[str] = None + emergency_contact_phone_number: typing.Optional[PhoneNumberType] = jstruct.JStruct[PhoneNumberType] + + +@attr.s(auto_attribs=True) +class TotalCostType: + currency: typing.Optional[str] = None + value: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class InsuranceType: + type: typing.Optional[str] = None + total_cost: typing.Optional[TotalCostType] = jstruct.JStruct[TotalCostType] + + +@attr.s(auto_attribs=True) +class CuboidType: + unit: typing.Optional[str] = None + l: typing.Optional[int] = None + w: typing.Optional[int] = None + h: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class PackageMeasurementsType: + weight: typing.Optional[WeightType] = jstruct.JStruct[WeightType] + cuboid: typing.Optional[CuboidType] = jstruct.JStruct[CuboidType] + + +@attr.s(auto_attribs=True) +class PackageType: + measurements: typing.Optional[PackageMeasurementsType] = jstruct.JStruct[PackageMeasurementsType] + description: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class InBondDetailsType: + type: typing.Optional[str] = None + name: typing.Optional[str] = None + address: typing.Optional[str] = None + contact_method: typing.Optional[str] = None + contact_email_address: typing.Optional[str] = None + contact_phone_number: typing.Optional[PhoneNumberType] = jstruct.JStruct[PhoneNumberType] + + +@attr.s(auto_attribs=True) +class PalletServiceDetailsType: + limited_access_delivery_type: typing.Optional[str] = None + limited_access_delivery_other_name: typing.Optional[str] = None + in_bond: typing.Optional[bool] = None + in_bond_details: typing.Optional[InBondDetailsType] = jstruct.JStruct[InBondDetailsType] + appointment_delivery: typing.Optional[bool] = None + protect_from_freeze: typing.Optional[bool] = None + threshold_pickup: typing.Optional[bool] = None + threshold_delivery: typing.Optional[bool] = None + + +@attr.s(auto_attribs=True) +class PalletType: + measurements: typing.Optional[PackageMeasurementsType] = jstruct.JStruct[PackageMeasurementsType] + description: typing.Optional[str] = None + freight_class: typing.Optional[str] = None + nmfc: typing.Optional[str] = None + contents_type: typing.Optional[str] = None + num_pieces: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class PackagingPropertiesType: + pallet_type: typing.Optional[str] = None + has_stackable_pallets: typing.Optional[bool] = None + dangerous_goods: typing.Optional[str] = None + dangerous_goods_details: typing.Optional[DangerousGoodsDetailsType] = jstruct.JStruct[DangerousGoodsDetailsType] + pallets: typing.Optional[typing.List[PalletType]] = jstruct.JList[PalletType] + packages: typing.Optional[typing.List[PackageType]] = jstruct.JList[PackageType] + courierpaks: typing.Optional[typing.List[CourierpakType]] = jstruct.JList[CourierpakType] + includes_return_label: typing.Optional[bool] = None + special_handling_required: typing.Optional[bool] = None + has_dangerous_goods: typing.Optional[bool] = None + pallet_service_details: typing.Optional[PalletServiceDetailsType] = jstruct.JStruct[PalletServiceDetailsType] + insurance: typing.Optional[InsuranceType] = jstruct.JStruct[InsuranceType] + + +@attr.s(auto_attribs=True) +class DetailsType: + origin: typing.Optional[DestinationType] = jstruct.JStruct[DestinationType] + destination: typing.Optional[DestinationType] = jstruct.JStruct[DestinationType] + expected_ship_date: typing.Optional[ExpectedShipDateType] = jstruct.JStruct[ExpectedShipDateType] + packaging_type: typing.Optional[str] = None + packaging_properties: typing.Optional[PackagingPropertiesType] = jstruct.JStruct[PackagingPropertiesType] + reference_codes: typing.Optional[typing.List[str]] = None + + +@attr.s(auto_attribs=True) +class RateRequestType: + services: typing.Optional[typing.List[str]] = None + excluded_services: typing.Optional[typing.List[str]] = None + details: typing.Optional[DetailsType] = jstruct.JStruct[DetailsType] diff --git a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_response.py b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_response.py new file mode 100644 index 0000000..964ba1b --- /dev/null +++ b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_response.py @@ -0,0 +1,49 @@ +import attr +import jstruct +import typing + + +@attr.s(auto_attribs=True) +class BaseType: + currency: typing.Optional[str] = None + value: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class SurchargeType: + type: typing.Optional[str] = None + amount: typing.Optional[BaseType] = jstruct.JStruct[BaseType] + + +@attr.s(auto_attribs=True) +class ValidUntilType: + year: typing.Optional[int] = None + month: typing.Optional[int] = None + day: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class RateType: + carrier_name: typing.Optional[str] = None + service_name: typing.Optional[str] = None + service_id: typing.Optional[str] = None + valid_until: typing.Optional[ValidUntilType] = jstruct.JStruct[ValidUntilType] + total: typing.Optional[BaseType] = jstruct.JStruct[BaseType] + base: typing.Optional[BaseType] = jstruct.JStruct[BaseType] + surcharges: typing.Optional[typing.List[SurchargeType]] = jstruct.JList[SurchargeType] + taxes: typing.Optional[typing.List[SurchargeType]] = jstruct.JList[SurchargeType] + transit_time_days: typing.Optional[int] = None + transit_time_not_available: typing.Optional[bool] = None + + +@attr.s(auto_attribs=True) +class StatusType: + done: typing.Optional[bool] = None + total: typing.Optional[int] = None + complete: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class RateResponseType: + status: typing.Optional[StatusType] = jstruct.JStruct[StatusType] + rates: typing.Optional[typing.List[RateType]] = jstruct.JList[RateType] diff --git a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_request.py b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_request.py new file mode 100644 index 0000000..ce35f92 --- /dev/null +++ b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_request.py @@ -0,0 +1,246 @@ +import attr +import jstruct +import typing + + +@attr.s(auto_attribs=True) +class NumberType: + number: typing.Optional[str] = None + extension: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class BrokerType: + use_carrier: typing.Optional[bool] = None + name: typing.Optional[str] = None + account_number: typing.Optional[str] = None + phone_number: typing.Optional[NumberType] = jstruct.JStruct[NumberType] + fax_number: typing.Optional[NumberType] = jstruct.JStruct[NumberType] + email_address: typing.Optional[str] = None + usmca_number: typing.Optional[str] = None + fda_number: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class TotalCostType: + currency: typing.Optional[str] = None + value: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class WeightType: + unit: typing.Optional[str] = None + value: typing.Optional[float] = None + + +@attr.s(auto_attribs=True) +class ProductType: + product_name: typing.Optional[str] = None + weight: typing.Optional[WeightType] = jstruct.JStruct[WeightType] + hs_code: typing.Optional[str] = None + country_of_origin: typing.Optional[str] = None + num_units: typing.Optional[int] = None + unit_price: typing.Optional[TotalCostType] = jstruct.JStruct[TotalCostType] + description: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class AddressType: + address_line_1: typing.Optional[str] = None + address_line_2: typing.Optional[str] = None + unit_number: typing.Optional[str] = None + city: typing.Optional[str] = None + region: typing.Optional[str] = None + country: typing.Optional[str] = None + postal_code: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class TaxRecipientType: + type: typing.Optional[str] = None + shipper_tax_identifier: typing.Optional[str] = None + receiver_tax_identifier: typing.Optional[str] = None + third_party_tax_identifier: typing.Optional[str] = None + other_tax_identifier: typing.Optional[str] = None + name: typing.Optional[str] = None + address: typing.Optional[AddressType] = jstruct.JStruct[AddressType] + phone_number: typing.Optional[NumberType] = jstruct.JStruct[NumberType] + reason_for_export: typing.Optional[str] = None + additional_remarks: typing.Optional[str] = None + comments: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class CustomsInvoiceDetailsType: + tax_recipient: typing.Optional[TaxRecipientType] = jstruct.JStruct[TaxRecipientType] + products: typing.Optional[typing.List[ProductType]] = jstruct.JList[ProductType] + + +@attr.s(auto_attribs=True) +class CustomsInvoiceType: + source: typing.Optional[str] = None + broker: typing.Optional[BrokerType] = jstruct.JStruct[BrokerType] + details: typing.Optional[CustomsInvoiceDetailsType] = jstruct.JStruct[CustomsInvoiceDetailsType] + + +@attr.s(auto_attribs=True) +class ReadyType: + hour: typing.Optional[int] = None + minute: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class DestinationType: + name: typing.Optional[str] = None + address: typing.Optional[AddressType] = jstruct.JStruct[AddressType] + residential: typing.Optional[bool] = None + tailgate_required: typing.Optional[bool] = None + instructions: typing.Optional[str] = None + contact_name: typing.Optional[str] = None + phone_number: typing.Optional[NumberType] = jstruct.JStruct[NumberType] + email_addresses: typing.Optional[typing.List[str]] = None + receives_email_updates: typing.Optional[bool] = None + ready_at: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + ready_until: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + signature_requirement: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class DateType: + year: typing.Optional[int] = None + month: typing.Optional[int] = None + day: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class InsuranceType: + type: typing.Optional[str] = None + total_cost: typing.Optional[TotalCostType] = jstruct.JStruct[TotalCostType] + + +@attr.s(auto_attribs=True) +class CourierpakMeasurementsType: + weight: typing.Optional[WeightType] = jstruct.JStruct[WeightType] + + +@attr.s(auto_attribs=True) +class CourierpakType: + measurements: typing.Optional[CourierpakMeasurementsType] = jstruct.JStruct[CourierpakMeasurementsType] + description: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class DangerousGoodsDetailsType: + packaging_group: typing.Optional[str] = None + goods_class: typing.Optional[str] = None + description: typing.Optional[str] = None + united_nations_number: typing.Optional[str] = None + emergency_contact_name: typing.Optional[str] = None + emergency_contact_phone_number: typing.Optional[NumberType] = jstruct.JStruct[NumberType] + + +@attr.s(auto_attribs=True) +class CuboidType: + unit: typing.Optional[str] = None + l: typing.Optional[int] = None + w: typing.Optional[int] = None + h: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class PackageMeasurementsType: + weight: typing.Optional[WeightType] = jstruct.JStruct[WeightType] + cuboid: typing.Optional[CuboidType] = jstruct.JStruct[CuboidType] + + +@attr.s(auto_attribs=True) +class PackageType: + measurements: typing.Optional[PackageMeasurementsType] = jstruct.JStruct[PackageMeasurementsType] + description: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class InBondDetailsType: + type: typing.Optional[str] = None + name: typing.Optional[str] = None + address: typing.Optional[str] = None + contact_method: typing.Optional[str] = None + contact_email_address: typing.Optional[str] = None + contact_phone_number: typing.Optional[NumberType] = jstruct.JStruct[NumberType] + + +@attr.s(auto_attribs=True) +class PalletServiceDetailsType: + limited_access_delivery_type: typing.Optional[str] = None + limited_access_delivery_other_name: typing.Optional[str] = None + in_bond: typing.Optional[bool] = None + in_bond_details: typing.Optional[InBondDetailsType] = jstruct.JStruct[InBondDetailsType] + appointment_delivery: typing.Optional[bool] = None + protect_from_freeze: typing.Optional[bool] = None + threshold_pickup: typing.Optional[bool] = None + threshold_delivery: typing.Optional[bool] = None + + +@attr.s(auto_attribs=True) +class PalletType: + measurements: typing.Optional[PackageMeasurementsType] = jstruct.JStruct[PackageMeasurementsType] + description: typing.Optional[str] = None + freight_class: typing.Optional[str] = None + nmfc: typing.Optional[str] = None + contents_type: typing.Optional[str] = None + num_pieces: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class PackagingPropertiesType: + pallet_type: typing.Optional[str] = None + has_stackable_pallets: typing.Optional[bool] = None + dangerous_goods: typing.Optional[str] = None + dangerous_goods_details: typing.Optional[DangerousGoodsDetailsType] = jstruct.JStruct[DangerousGoodsDetailsType] + pallets: typing.Optional[typing.List[PalletType]] = jstruct.JList[PalletType] + packages: typing.Optional[typing.List[PackageType]] = jstruct.JList[PackageType] + courierpaks: typing.Optional[typing.List[CourierpakType]] = jstruct.JList[CourierpakType] + includes_return_label: typing.Optional[bool] = None + special_handling_required: typing.Optional[bool] = None + has_dangerous_goods: typing.Optional[bool] = None + pallet_service_details: typing.Optional[PalletServiceDetailsType] = jstruct.JStruct[PalletServiceDetailsType] + + +@attr.s(auto_attribs=True) +class ShipmentRequestDetailsType: + origin: typing.Optional[DestinationType] = jstruct.JStruct[DestinationType] + destination: typing.Optional[DestinationType] = jstruct.JStruct[DestinationType] + expected_ship_date: typing.Optional[DateType] = jstruct.JStruct[DateType] + packaging_type: typing.Optional[str] = None + packaging_properties: typing.Optional[PackagingPropertiesType] = jstruct.JStruct[PackagingPropertiesType] + insurance: typing.Optional[InsuranceType] = jstruct.JStruct[InsuranceType] + reference_codes: typing.Optional[typing.List[str]] = None + + +@attr.s(auto_attribs=True) +class DispatchDetailsType: + date: typing.Optional[DateType] = jstruct.JStruct[DateType] + ready_at: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + ready_until: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + + +@attr.s(auto_attribs=True) +class PickupDetailsType: + pre_scheduled_pickup: typing.Optional[bool] = None + date: typing.Optional[DateType] = jstruct.JStruct[DateType] + ready_at: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + ready_until: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + pickup_location: typing.Optional[str] = None + contact_name: typing.Optional[str] = None + contact_phone_number: typing.Optional[NumberType] = jstruct.JStruct[NumberType] + + +@attr.s(auto_attribs=True) +class ShipmentRequestType: + unique_id: typing.Optional[str] = None + payment_method_id: typing.Optional[str] = None + service_id: typing.Optional[str] = None + details: typing.Optional[ShipmentRequestDetailsType] = jstruct.JStruct[ShipmentRequestDetailsType] + customs_invoice: typing.Optional[CustomsInvoiceType] = jstruct.JStruct[CustomsInvoiceType] + pickup_details: typing.Optional[PickupDetailsType] = jstruct.JStruct[PickupDetailsType] + dispatch_details: typing.Optional[DispatchDetailsType] = jstruct.JStruct[DispatchDetailsType] diff --git a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_response.py b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_response.py new file mode 100644 index 0000000..2e57baf --- /dev/null +++ b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_response.py @@ -0,0 +1,224 @@ +import attr +import jstruct +import typing + + +@attr.s(auto_attribs=True) +class AddressType: + address_line_1: typing.Optional[str] = None + address_line_2: typing.Optional[str] = None + unit_number: typing.Optional[str] = None + city: typing.Optional[str] = None + region: typing.Optional[str] = None + country: typing.Optional[str] = None + postal_code: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class PhoneNumberType: + number: typing.Optional[str] = None + extension: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class ReadyType: + hour: typing.Optional[int] = None + minute: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class DestinationType: + name: typing.Optional[str] = None + address: typing.Optional[AddressType] = jstruct.JStruct[AddressType] + residential: typing.Optional[bool] = None + tailgate_required: typing.Optional[bool] = None + instructions: typing.Optional[str] = None + contact_name: typing.Optional[str] = None + phone_number: typing.Optional[PhoneNumberType] = jstruct.JStruct[PhoneNumberType] + email_addresses: typing.Optional[typing.List[str]] = None + receives_email_updates: typing.Optional[bool] = None + ready_at: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + ready_until: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] + signature_requirement: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class ExpectedShipDateType: + year: typing.Optional[int] = None + month: typing.Optional[int] = None + day: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class BaseType: + currency: typing.Optional[str] = None + value: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class InsuranceType: + type: typing.Optional[str] = None + total_cost: typing.Optional[BaseType] = jstruct.JStruct[BaseType] + + +@attr.s(auto_attribs=True) +class WeightType: + unit: typing.Optional[str] = None + value: typing.Optional[float] = None + + +@attr.s(auto_attribs=True) +class CourierpakMeasurementsType: + weight: typing.Optional[WeightType] = jstruct.JStruct[WeightType] + + +@attr.s(auto_attribs=True) +class CourierpakType: + measurements: typing.Optional[CourierpakMeasurementsType] = jstruct.JStruct[CourierpakMeasurementsType] + description: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class DangerousGoodsDetailsType: + packaging_group: typing.Optional[str] = None + goods_class: typing.Optional[str] = None + description: typing.Optional[str] = None + united_nations_number: typing.Optional[str] = None + emergency_contact_name: typing.Optional[str] = None + emergency_contact_phone_number: typing.Optional[PhoneNumberType] = jstruct.JStruct[PhoneNumberType] + + +@attr.s(auto_attribs=True) +class CuboidType: + unit: typing.Optional[str] = None + l: typing.Optional[int] = None + w: typing.Optional[int] = None + h: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class PackageMeasurementsType: + weight: typing.Optional[WeightType] = jstruct.JStruct[WeightType] + cuboid: typing.Optional[CuboidType] = jstruct.JStruct[CuboidType] + + +@attr.s(auto_attribs=True) +class PackageType: + measurements: typing.Optional[PackageMeasurementsType] = jstruct.JStruct[PackageMeasurementsType] + description: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class InBondDetailsType: + type: typing.Optional[str] = None + name: typing.Optional[str] = None + address: typing.Optional[str] = None + contact_method: typing.Optional[str] = None + contact_email_address: typing.Optional[str] = None + contact_phone_number: typing.Optional[PhoneNumberType] = jstruct.JStruct[PhoneNumberType] + + +@attr.s(auto_attribs=True) +class PalletServiceDetailsType: + limited_access_delivery_type: typing.Optional[str] = None + limited_access_delivery_other_name: typing.Optional[str] = None + in_bond: typing.Optional[bool] = None + in_bond_details: typing.Optional[InBondDetailsType] = jstruct.JStruct[InBondDetailsType] + appointment_delivery: typing.Optional[bool] = None + protect_from_freeze: typing.Optional[bool] = None + threshold_pickup: typing.Optional[bool] = None + threshold_delivery: typing.Optional[bool] = None + + +@attr.s(auto_attribs=True) +class PalletType: + measurements: typing.Optional[PackageMeasurementsType] = jstruct.JStruct[PackageMeasurementsType] + description: typing.Optional[str] = None + freight_class: typing.Optional[str] = None + nmfc: typing.Optional[str] = None + contents_type: typing.Optional[str] = None + num_pieces: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class PackagingPropertiesType: + pallet_type: typing.Optional[str] = None + has_stackable_pallets: typing.Optional[bool] = None + dangerous_goods: typing.Optional[str] = None + dangerous_goods_details: typing.Optional[DangerousGoodsDetailsType] = jstruct.JStruct[DangerousGoodsDetailsType] + pallets: typing.Optional[typing.List[PalletType]] = jstruct.JList[PalletType] + packages: typing.Optional[typing.List[PackageType]] = jstruct.JList[PackageType] + courierpaks: typing.Optional[typing.List[CourierpakType]] = jstruct.JList[CourierpakType] + includes_return_label: typing.Optional[bool] = None + special_handling_required: typing.Optional[bool] = None + has_dangerous_goods: typing.Optional[bool] = None + pallet_service_details: typing.Optional[PalletServiceDetailsType] = jstruct.JStruct[PalletServiceDetailsType] + + +@attr.s(auto_attribs=True) +class DetailsType: + origin: typing.Optional[DestinationType] = jstruct.JStruct[DestinationType] + destination: typing.Optional[DestinationType] = jstruct.JStruct[DestinationType] + expected_ship_date: typing.Optional[ExpectedShipDateType] = jstruct.JStruct[ExpectedShipDateType] + packaging_type: typing.Optional[str] = None + packaging_properties: typing.Optional[PackagingPropertiesType] = jstruct.JStruct[PackagingPropertiesType] + insurance: typing.Optional[InsuranceType] = jstruct.JStruct[InsuranceType] + reference_codes: typing.Optional[typing.List[str]] = None + + +@attr.s(auto_attribs=True) +class LabelType: + size: typing.Optional[str] = None + format: typing.Optional[str] = None + url: typing.Optional[str] = None + padded: typing.Optional[bool] = None + + +@attr.s(auto_attribs=True) +class SurchargeType: + type: typing.Optional[str] = None + amount: typing.Optional[BaseType] = jstruct.JStruct[BaseType] + + +@attr.s(auto_attribs=True) +class RateType: + carrier_name: typing.Optional[str] = None + service_name: typing.Optional[str] = None + service_id: typing.Optional[str] = None + valid_until: typing.Optional[ExpectedShipDateType] = jstruct.JStruct[ExpectedShipDateType] + total: typing.Optional[BaseType] = jstruct.JStruct[BaseType] + base: typing.Optional[BaseType] = jstruct.JStruct[BaseType] + surcharges: typing.Optional[typing.List[SurchargeType]] = jstruct.JList[SurchargeType] + taxes: typing.Optional[typing.List[SurchargeType]] = jstruct.JList[SurchargeType] + transit_time_days: typing.Optional[int] = None + transit_time_not_available: typing.Optional[bool] = None + + +@attr.s(auto_attribs=True) +class TransportDataType: + pass + + +@attr.s(auto_attribs=True) +class ShipmentType: + id: typing.Optional[str] = None + unique_id: typing.Optional[str] = None + state: typing.Optional[str] = None + transaction_number: typing.Optional[str] = None + primary_tracking_number: typing.Optional[str] = None + tracking_numbers: typing.Optional[typing.List[str]] = None + tracking_url: typing.Optional[str] = None + return_tracking_number: typing.Optional[str] = None + bol_number: typing.Optional[str] = None + pickup_confirmation_number: typing.Optional[str] = None + details: typing.Optional[DetailsType] = jstruct.JStruct[DetailsType] + transport_data: typing.Optional[TransportDataType] = jstruct.JStruct[TransportDataType] + labels: typing.Optional[typing.List[LabelType]] = jstruct.JList[LabelType] + customs_invoice_url: typing.Optional[str] = None + rate: typing.Optional[RateType] = jstruct.JStruct[RateType] + order_source: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class ShipmentResponseType: + shipment: typing.Optional[ShipmentType] = jstruct.JStruct[ShipmentType] diff --git a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/tracking_response.py b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/tracking_response.py new file mode 100644 index 0000000..601b404 --- /dev/null +++ b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/tracking_response.py @@ -0,0 +1,23 @@ +import attr +import jstruct +import typing + + +@attr.s(auto_attribs=True) +class WhereType: + city: typing.Optional[str] = None + region: typing.Optional[str] = None + country: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class EventType: + type: typing.Optional[str] = None + when: typing.Optional[str] = None + where: typing.Optional[WhereType] = jstruct.JStruct[WhereType] + message: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class TrackingResponseType: + events: typing.Optional[typing.List[EventType]] = jstruct.JList[EventType] diff --git a/plugins/freightcom_rest/pyproject.toml b/plugins/freightcom_rest/pyproject.toml new file mode 100644 index 0000000..8cb9cb3 --- /dev/null +++ b/plugins/freightcom_rest/pyproject.toml @@ -0,0 +1,39 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "karrio_freightcom_rest" +version = "2025.8" +description = "Karrio - Freightcom Rest Shipping Extension" +readme = "README.md" +requires-python = ">=3.11" +license = "Apache-2.0" +authors = [ + {name = "karrio", email = "hello@karrio.io"} +] +classifiers = [ + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", +] +dependencies = [ + "karrio", +] + +[project.urls] +Homepage = "https://github.com/karrioapi/karrio" + +[project.entry-points."karrio.plugins"] +freightcom_rest = "karrio.plugins.freightcom_rest" + +[tool.setuptools] +zip-safe = false +include-package-data = true + +[tool.setuptools.package-dir] +"" = "." + +[tool.setuptools.packages.find] +exclude = ["tests.*", "tests"] +namespaces = true \ No newline at end of file diff --git a/plugins/freightcom_rest/schemas/error_response.json b/plugins/freightcom_rest/schemas/error_response.json new file mode 100644 index 0000000..0959a23 --- /dev/null +++ b/plugins/freightcom_rest/schemas/error_response.json @@ -0,0 +1,4 @@ +{ +"message": "string", + "data": {"services":"invalid-syntax"} +} diff --git a/plugins/freightcom_rest/schemas/pickup_request.json b/plugins/freightcom_rest/schemas/pickup_request.json new file mode 100644 index 0000000..7c5b184 --- /dev/null +++ b/plugins/freightcom_rest/schemas/pickup_request.json @@ -0,0 +1,39 @@ +{ + "pickup_details": { + "pre_scheduled_pickup": true, + "date": { + "year": 2006, + "month": 6, + "day": 7 + }, + "ready_at": { + "hour": 15, + "minute": 6 + }, + "ready_until": { + "hour": 15, + "minute": 6 + }, + "pickup_location": "string", + "contact_name": "string", + "contact_phone_number": { + "number": "5554447777", + "extension": "123" + } + }, + "dispatch_details": { + "date": { + "year": 2006, + "month": 6, + "day": 7 + }, + "ready_at": { + "hour": 15, + "minute": 6 + }, + "ready_until": { + "hour": 15, + "minute": 6 + } + } +} diff --git a/plugins/freightcom_rest/schemas/rate_request.json b/plugins/freightcom_rest/schemas/rate_request.json new file mode 100644 index 0000000..55b90a3 --- /dev/null +++ b/plugins/freightcom_rest/schemas/rate_request.json @@ -0,0 +1,171 @@ +{ + "services": [ + "string" + ], + "excluded_services": [ + "string" + ], + "details": { + "origin": { + "name": "Philip J Fry", + "address": { + "address_line_1": "200 University Avenue West", + "address_line_2": "string", + "unit_number": "42a", + "city": "Waterloo", + "region": "ON", + "country": "CA", + "postal_code": "string" + }, + "residential": true, + "tailgate_required": true, + "instructions": "string", + "contact_name": "string", + "phone_number": { + "number": "5554447777", + "extension": "123" + }, + "email_addresses": [ + "user@example.com" + ], + "receives_email_updates": true + }, + "destination": { + "name": "Philip J Fry", + "address": { + "address_line_1": "200 University Avenue West", + "address_line_2": "string", + "unit_number": "42a", + "city": "Waterloo", + "region": "ON", + "country": "CA", + "postal_code": "string" + }, + "residential": true, + "tailgate_required": true, + "instructions": "string", + "contact_name": "string", + "phone_number": { + "number": "5554447777", + "extension": "123" + }, + "email_addresses": [ + "user@example.com" + ], + "receives_email_updates": true, + "ready_at": { + "hour": 15, + "minute": 6 + }, + "ready_until": { + "hour": 15, + "minute": 6 + }, + "signature_requirement": "not-required" + }, + "expected_ship_date": { + "year": 2006, + "month": 6, + "day": 7 + }, + "packaging_type": "pallet", + "packaging_properties": { + "pallet_type": "ltl", + "has_stackable_pallets": true, + "dangerous_goods": "limited-quantity", + "dangerous_goods_details": { + "packaging_group": "string", + "goods_class": "string", + "description": "string", + "united_nations_number": "string", + "emergency_contact_name": "string", + "emergency_contact_phone_number": { + "number": "5554447777", + "extension": "123" + } + }, + "pallets": [ + { + "measurements": { + "weight": { + "unit": "lb", + "value": 2.95 + }, + "cuboid": { + "unit": "ft", + "l": 5, + "w": 5, + "h": 5 + } + }, + "description": "string", + "freight_class": "string", + "nmfc": "string", + "contents_type": "string", + "num_pieces": 0 + } + ], + "packages": [ + { + "measurements": { + "weight": { + "unit": "lb", + "value": 2.95 + }, + "cuboid": { + "unit": "ft", + "l": 5, + "w": 5, + "h": 5 + } + }, + "description": "string" + } + ], + "courierpaks": [ + { + "measurements": { + "weight": { + "unit": "lb", + "value": 2.95 + } + }, + "description": "string" + } + ], + "includes_return_label": false, + "special_handling_required": false, + "has_dangerous_goods": false, + "pallet_service_details": { + "limited_access_delivery_type": "string", + "limited_access_delivery_other_name": "string", + "in_bond": true, + "in_bond_details": { + "type": "immediate-exportation", + "name": "string", + "address": "string", + "contact_method": "email-address", + "contact_email_address": "string", + "contact_phone_number": { + "number": "5554447777", + "extension": "123" + } + }, + "appointment_delivery": true, + "protect_from_freeze": true, + "threshold_pickup": true, + "threshold_delivery": true + }, + "insurance": { + "type": "internal", + "total_cost": { + "currency": "CAD", + "value": "4250" + } + } + }, + "reference_codes": [ + "string" + ] + } +} diff --git a/plugins/freightcom_rest/schemas/rate_response.json b/plugins/freightcom_rest/schemas/rate_response.json new file mode 100644 index 0000000..db82e35 --- /dev/null +++ b/plugins/freightcom_rest/schemas/rate_response.json @@ -0,0 +1,47 @@ +{ + "status": { + "done": true, + "total": 0, + "complete": 0 + }, + "rates": [ + { + "carrier_name": "string", + "service_name": "string", + "service_id": "string", + "valid_until": { + "year": 2006, + "month": 6, + "day": 7 + }, + "total": { + "currency": "CAD", + "value": "4250" + }, + "base": { + "currency": "CAD", + "value": "4250" + }, + "surcharges": [ + { + "type": "fuel", + "amount": { + "currency": "CAD", + "value": "4250" + } + } + ], + "taxes": [ + { + "type": "fuel", + "amount": { + "currency": "CAD", + "value": "4250" + } + } + ], + "transit_time_days": 5, + "transit_time_not_available": true + } + ] +} diff --git a/plugins/freightcom_rest/schemas/shipment_request.json b/plugins/freightcom_rest/schemas/shipment_request.json new file mode 100644 index 0000000..15a543f --- /dev/null +++ b/plugins/freightcom_rest/schemas/shipment_request.json @@ -0,0 +1,267 @@ +{ + "unique_id": "string", + "payment_method_id": "string", + "service_id": "string", + "details": { + "origin": { + "name": "Philip J Fry", + "address": { + "address_line_1": "200 University Avenue West", + "address_line_2": "string", + "unit_number": "42a", + "city": "Waterloo", + "region": "ON", + "country": "CA", + "postal_code": "string" + }, + "residential": true, + "tailgate_required": true, + "instructions": "string", + "contact_name": "string", + "phone_number": { + "number": "5554447777", + "extension": "123" + }, + "email_addresses": [ + "user@example.com" + ], + "receives_email_updates": true + }, + "destination": { + "name": "Philip J Fry", + "address": { + "address_line_1": "200 University Avenue West", + "address_line_2": "string", + "unit_number": "42a", + "city": "Waterloo", + "region": "ON", + "country": "CA", + "postal_code": "string" + }, + "residential": true, + "tailgate_required": true, + "instructions": "string", + "contact_name": "string", + "phone_number": { + "number": "5554447777", + "extension": "123" + }, + "email_addresses": [ + "user@example.com" + ], + "receives_email_updates": true, + "ready_at": { + "hour": 15, + "minute": 6 + }, + "ready_until": { + "hour": 15, + "minute": 6 + }, + "signature_requirement": "not-required" + }, + "expected_ship_date": { + "year": 2006, + "month": 6, + "day": 7 + }, + "packaging_type": "pallet", + "packaging_properties": { + "pallet_type": "ltl", + "has_stackable_pallets": true, + "dangerous_goods": "limited-quantity", + "dangerous_goods_details": { + "packaging_group": "string", + "goods_class": "string", + "description": "string", + "united_nations_number": "string", + "emergency_contact_name": "string", + "emergency_contact_phone_number": { + "number": "5554447777", + "extension": "123" + } + }, + "pallets": [ + { + "measurements": { + "weight": { + "unit": "lb", + "value": 2.95 + }, + "cuboid": { + "unit": "ft", + "l": 5, + "w": 5, + "h": 5 + } + }, + "description": "string", + "freight_class": "string", + "nmfc": "string", + "contents_type": "string", + "num_pieces": 0 + } + ], + "packages": [ + { + "measurements": { + "weight": { + "unit": "lb", + "value": 2.95 + }, + "cuboid": { + "unit": "ft", + "l": 5, + "w": 5, + "h": 5 + } + }, + "description": "string" + } + ], + "courierpaks": [ + { + "measurements": { + "weight": { + "unit": "lb", + "value": 2.95 + } + }, + "description": "string" + } + ], + "includes_return_label": false, + "special_handling_required": false, + "has_dangerous_goods": false, + "pallet_service_details": { + "limited_access_delivery_type": "construction-site", + "limited_access_delivery_other_name": "string", + "in_bond": true, + "in_bond_details": { + "type": "immediate-exportation", + "name": "string", + "address": "string", + "contact_method": "email-address", + "contact_email_address": "string", + "contact_phone_number": { + "number": "5554447777", + "extension": "123" + } + }, + "appointment_delivery": true, + "protect_from_freeze": true, + "threshold_pickup": true, + "threshold_delivery": true + } + }, + "insurance": { + "type": "internal", + "total_cost": { + "currency": "CAD", + "value": "4250" + } + }, + "reference_codes": [ + "string" + ] + }, + "customs_invoice": { + "source": "details", + "broker": { + "use_carrier": true, + "name": "string", + "account_number": "string", + "phone_number": { + "number": "5554447777", + "extension": "123" + }, + "fax_number": { + "number": "5554447777", + "extension": "123" + }, + "email_address": "string", + "usmca_number": "string", + "fda_number": "string" + }, + "details": { + "tax_recipient": { + "type": "shipper", + "shipper_tax_identifier": "string", + "receiver_tax_identifier": "string", + "third_party_tax_identifier": "string", + "other_tax_identifier": "string", + "name": "string", + "address": { + "address_line_1": "200 University Avenue West", + "address_line_2": "string", + "unit_number": "42a", + "city": "Waterloo", + "region": "ON", + "country": "CA", + "postal_code": "string" + }, + "phone_number": { + "number": "5554447777", + "extension": "123" + }, + "reason_for_export": "gift", + "additional_remarks": "string", + "comments": "string" + }, + "products": [ + { + "product_name": "string", + "weight": { + "unit": "lb", + "value": 2.95 + }, + "hs_code": "string", + "country_of_origin": "CA", + "num_units": 1, + "unit_price": { + "currency": "CAD", + "value": "4250" + }, + "description": "string" + } + ] + } + }, + "pickup_details": { + "pre_scheduled_pickup": true, + "date": { + "year": 2006, + "month": 6, + "day": 7 + }, + "ready_at": { + "hour": 15, + "minute": 6 + }, + "ready_until": { + "hour": 15, + "minute": 6 + }, + "pickup_location": "string", + "contact_name": "string", + "contact_phone_number": { + "number": "5554447777", + "extension": "123" + } + }, + "dispatch_details": { + "date": { + "year": 2006, + "month": 6, + "day": 7 + }, + "ready_at": { + "hour": 15, + "minute": 6 + }, + "ready_until": { + "hour": 15, + "minute": 6 + } + } +} diff --git a/plugins/freightcom_rest/schemas/shipment_response.json b/plugins/freightcom_rest/schemas/shipment_response.json new file mode 100644 index 0000000..7a32f87 --- /dev/null +++ b/plugins/freightcom_rest/schemas/shipment_response.json @@ -0,0 +1,228 @@ +{ + "shipment": { + "id": "string", + "unique_id": "string", + "state": "draft", + "transaction_number": "string", + "primary_tracking_number": "string", + "tracking_numbers": [ + "string" + ], + "tracking_url": "string", + "return_tracking_number": "string", + "bol_number": "string", + "pickup_confirmation_number": "string", + "details": { + "origin": { + "name": "Philip J Fry", + "address": { + "address_line_1": "200 University Avenue West", + "address_line_2": "string", + "unit_number": "42a", + "city": "Waterloo", + "region": "ON", + "country": "CA", + "postal_code": "string" + }, + "residential": true, + "tailgate_required": true, + "instructions": "string", + "contact_name": "string", + "phone_number": { + "number": "5554447777", + "extension": "123" + }, + "email_addresses": [ + "user@example.com" + ], + "receives_email_updates": true + }, + "destination": { + "name": "Philip J Fry", + "address": { + "address_line_1": "200 University Avenue West", + "address_line_2": "string", + "unit_number": "42a", + "city": "Waterloo", + "region": "ON", + "country": "CA", + "postal_code": "string" + }, + "residential": true, + "tailgate_required": true, + "instructions": "string", + "contact_name": "string", + "phone_number": { + "number": "5554447777", + "extension": "123" + }, + "email_addresses": [ + "user@example.com" + ], + "receives_email_updates": true, + "ready_at": { + "hour": 15, + "minute": 6 + }, + "ready_until": { + "hour": 15, + "minute": 6 + }, + "signature_requirement": "not-required" + }, + "expected_ship_date": { + "year": 2006, + "month": 6, + "day": 7 + }, + "packaging_type": "pallet", + "packaging_properties": { + "pallet_type": "ltl", + "has_stackable_pallets": true, + "dangerous_goods": "limited-quantity", + "dangerous_goods_details": { + "packaging_group": "string", + "goods_class": "string", + "description": "string", + "united_nations_number": "string", + "emergency_contact_name": "string", + "emergency_contact_phone_number": { + "number": "5554447777", + "extension": "123" + } + }, + "pallets": [ + { + "measurements": { + "weight": { + "unit": "lb", + "value": 2.95 + }, + "cuboid": { + "unit": "ft", + "l": 5, + "w": 5, + "h": 5 + } + }, + "description": "string", + "freight_class": "string", + "nmfc": "string", + "contents_type": "string", + "num_pieces": 0 + } + ], + "packages": [ + { + "measurements": { + "weight": { + "unit": "lb", + "value": 2.95 + }, + "cuboid": { + "unit": "ft", + "l": 5, + "w": 5, + "h": 5 + } + }, + "description": "string" + } + ], + "courierpaks": [ + { + "measurements": { + "weight": { + "unit": "lb", + "value": 2.95 + } + }, + "description": "string" + } + ], + "includes_return_label": false, + "special_handling_required": false, + "has_dangerous_goods": false, + "pallet_service_details": { + "limited_access_delivery_type": "construction-site", + "limited_access_delivery_other_name": "string", + "in_bond": true, + "in_bond_details": { + "type": "immediate-exportation", + "name": "string", + "address": "string", + "contact_method": "email-address", + "contact_email_address": "string", + "contact_phone_number": { + "number": "5554447777", + "extension": "123" + } + }, + "appointment_delivery": true, + "protect_from_freeze": true, + "threshold_pickup": true, + "threshold_delivery": true + } + }, + "insurance": { + "type": "internal", + "total_cost": { + "currency": "CAD", + "value": "4250" + } + }, + "reference_codes": [ + "string" + ] + }, + "transport_data": {}, + "labels": [ + { + "size": "letter", + "format": "pdf", + "url": "string", + "padded": true + } + ], + "customs_invoice_url": "string", + "rate": { + "carrier_name": "string", + "service_name": "string", + "service_id": "string", + "valid_until": { + "year": 2006, + "month": 6, + "day": 7 + }, + "total": { + "currency": "CAD", + "value": "4250" + }, + "base": { + "currency": "CAD", + "value": "4250" + }, + "surcharges": [ + { + "type": "fuel", + "amount": { + "currency": "CAD", + "value": "4250" + } + } + ], + "taxes": [ + { + "type": "fuel", + "amount": { + "currency": "CAD", + "value": "4250" + } + } + ], + "transit_time_days": 5, + "transit_time_not_available": true + }, + "order_source": "string" + } +} diff --git a/plugins/freightcom_rest/schemas/tracking_response.json b/plugins/freightcom_rest/schemas/tracking_response.json new file mode 100644 index 0000000..b0310d6 --- /dev/null +++ b/plugins/freightcom_rest/schemas/tracking_response.json @@ -0,0 +1,14 @@ +{ + "events": [ + { + "type": "label-created", + "when": "string", + "where": { + "city": "string", + "region": "string", + "country": "string" + }, + "message": "string" + } + ] +} diff --git a/plugins/freightcom_rest/tests/__init__.py b/plugins/freightcom_rest/tests/__init__.py new file mode 100644 index 0000000..c041abf --- /dev/null +++ b/plugins/freightcom_rest/tests/__init__.py @@ -0,0 +1,4 @@ + +from freightcom_rest.test_rate import * +from freightcom_rest.test_tracking import * +from freightcom_rest.test_shipment import * \ No newline at end of file diff --git a/plugins/freightcom_rest/tests/freightcom_rest/__init__.py b/plugins/freightcom_rest/tests/freightcom_rest/__init__.py new file mode 100644 index 0000000..54814c8 --- /dev/null +++ b/plugins/freightcom_rest/tests/freightcom_rest/__init__.py @@ -0,0 +1 @@ +from .fixture import gateway \ No newline at end of file diff --git a/plugins/freightcom_rest/tests/freightcom_rest/fixture.py b/plugins/freightcom_rest/tests/freightcom_rest/fixture.py new file mode 100644 index 0000000..bb82a31 --- /dev/null +++ b/plugins/freightcom_rest/tests/freightcom_rest/fixture.py @@ -0,0 +1,22 @@ +"""Freightcom Rest carrier tests fixtures.""" + +import karrio.sdk as karrio +import karrio.lib as lib + +cached_payment_method_id = { + f"payment|freightcom_rest|net_terms|TEST_API_KEY": dict( + id="string", + type= "net-terms", + label="Net Terms" + ) +} +gateway = karrio.gateway["freightcom_rest"].create( + dict( + api_key="TEST_API_KEY", + config=dict( + payment_method_type="net_terms" + ), + ), + cache=lib.Cache(**cached_payment_method_id), + +) diff --git a/plugins/freightcom_rest/tests/freightcom_rest/test_rate.py b/plugins/freightcom_rest/tests/freightcom_rest/test_rate.py new file mode 100644 index 0000000..aad8935 --- /dev/null +++ b/plugins/freightcom_rest/tests/freightcom_rest/test_rate.py @@ -0,0 +1,458 @@ +"""Freightcom Rest carrier rate tests.""" +import datetime +import unittest +from unittest.mock import patch, ANY +from .fixture import gateway +import logging +import karrio.sdk as karrio +import karrio.lib as lib +import karrio.core.models as models + +logger = logging.getLogger(__name__) + + +class TestFreightcomRestRating(unittest.TestCase): + def setUp(self): + self.maxDiff = None + self.RateRequest = models.RateRequest(**RatePayload) + + def test_create_rate_request(self): + request = gateway.mapper.create_rate_request(self.RateRequest) + self.assertEqual(lib.to_dict(request.serialize()), RateRequest) + + def test_get_rates(self): + with patch("karrio.mappers.freightcom_rest.proxy.lib.request") as mock: + mock.return_value = "{}" + karrio.Rating.fetch(self.RateRequest).from_(gateway) + self.assertEqual( + mock.call_args[1]["url"], + f"{gateway.settings.server_url}/rate" + ) + + def test_parse_rate_response(self): + with patch("karrio.mappers.freightcom_rest.proxy.lib.request") as mock: + mock.return_value = RateResponse + parsed_response = ( + karrio.Rating.fetch(self.RateRequest) + .from_(gateway) + .parse() + ) + self.assertListEqual(lib.to_dict(parsed_response), ParsedRateResponse) + + def test_parse_error_response(self): + with patch("karrio.mappers.freightcom_rest.proxy.lib.request") as mock: + mock.return_value = ErrorResponse + parsed_response = ( + karrio.Rating.fetch(self.RateRequest) + .from_(gateway) + .parse() + ) + self.assertListEqual(lib.to_dict(parsed_response), ParsedErrorResponse) + + +if __name__ == "__main__": + unittest.main() + + +RatePayload = { + "shipper": { + "company_name": "Test Company - From", + "address_line1": "9, Van Der Graaf Court", + "city": "Brampton", + "postal_code": "L4T3T1", + "country_code": "CA", + "state_code": "ON", + "email": "shipper@example.com", + "phone_number": "(123) 114 1499" + }, + "recipient": { + "company_name": "Test Company - Destination", + "address_line1": "1410 Fall River Rd", + "city": "Fall River", + "country_code": "CA", + "postal_code": "B2T1J1", + "residential": "true", + "state_code": "NS", + "email": "recipient@example.com", + "phone_number": "(999) 999 9999" + }, + "parcels": [ + { + "height": 50, + "length": 50, + "weight": 20, + "width": 12, + "dimension_unit": "CM", + "weight_unit": "KG", + "description": "Package 1 Description" + }, + { + "height": 30, + "length": 50, + "weight": 20, + "width": 12, + "dimension_unit": "CM", + "weight_unit": "KG", + "description": "Package 2 Description" + } + ], + "reference": "REF-001", + "options": { + "email_notification": True, + "shipping_date": datetime.datetime(2025, 2, 25, 1,0).strftime("%Y-%m-%dT%H:%M"), + } +} + + +RateRequest = { + "details": { + "destination": { + "address": { + "address_line_1": "1410 Fall River Rd", + "city": "Fall River", + "country": "CA", + "postal_code": "B2T1J1", + "region": "NS", + }, + "email_addresses": ["recipient@example.com"], + 'name': 'Test Company - Destination', + "phone_number": {"number": "(999) 999 9999"}, + "ready_at": { + "hour": 10, "minute": 0 + }, + "ready_until": { + "hour": 17, "minute": 0 + }, + "receives_email_updates": True, + "residential": False, + "signature_requirement": "not-required" + }, + "origin": { + "address": { + "address_line_1": "9, Van Der Graaf Court", + "city": "Brampton", + "country": "CA", + "postal_code": "L4T3T1", + "region": "ON", + }, + "name": "Test Company - From", + "email_addresses": ["shipper@example.com"], + "phone_number": {"number": "(123) 114 1499"}, + "residential": False + }, + "expected_ship_date": {"day": 25, "month": 2, "year": 2025}, + "packaging_type": "package", + "packaging_properties": { + "packages": [ + { + "description": "Package 1 Description", + "measurements": { + "cuboid": { + "h": 50.0, + "l": 50.0, + "unit": "cm", + "w": 12.0 + }, + "weight": { + "unit": "kg", + "value": 20.0 + } + } + }, + { + "description": "Package 2 Description", + "measurements": { + "cuboid": { + "h": 30.0, + "l": 50.0, + "unit": "cm", + "w": 12.0 + }, + "weight": { + "unit": "kg", + "value": 20.0 + } + } + } + ], + }, + "reference_codes": ["REF-001"] + } +} + + +RateResponse = """ +{ + "status": { + "done": true, + "total": 99, + "complete": 99 + }, + "rates": [ + { + "service_id": "canpar.ground", + "valid_until": { + "year": 2025, + "month": 3, + "day": 3 + }, + "total": { + "value": "5334", + "currency": "CAD" + }, + "base": { + "value": "3368", + "currency": "CAD" + }, + "surcharges": [ + { + "type": "fuel", + "amount": { + "value": "1001", + "currency": "CAD" + } + }, + { + "type": "carbon-surcharge", + "amount": { + "value": "51", + "currency": "CAD" + } + }, + { + "type": "residential-delivery", + "amount": { + "value": "218", + "currency": "CAD" + } + } + ], + "taxes": [ + { + "type": "tax-hst-ns", + "amount": { + "value": "696", + "currency": "CAD" + } + } + ], + "transit_time_days": 2, + "transit_time_not_available": false, + "carrier_name": "Canpar", + "service_name": "Ground" + }, + { + "service_id": "fedex-courier.ground", + "valid_until": { + "year": 2025, + "month": 3, + "day": 3 + }, + "total": { + "value": "6462", + "currency": "CAD" + }, + "base": { + "value": "4087", + "currency": "CAD" + }, + "surcharges": [ + { + "type": "fuel", + "amount": { + "value": "1254", + "currency": "CAD" + } + }, + { + "type": "residential-delivery", + "amount": { + "value": "278", + "currency": "CAD" + } + } + ], + "taxes": [ + { + "type": "tax-hst-ns", + "amount": { + "value": "843", + "currency": "CAD" + } + } + ], + "transit_time_days": 2, + "transit_time_not_available": false, + "carrier_name": "FedEx Courier", + "service_name": "Ground" + }, + { + "service_id": "purolatorcourier.ground", + "valid_until": { + "year": 2025, + "month": 3, + "day": 3 + }, + "total": { + "value": "6047", + "currency": "CAD" + }, + "base": { + "value": "4134", + "currency": "CAD" + }, + "surcharges": [ + { + "type": "fuel", + "amount": { + "value": "1124", + "currency": "CAD" + } + } + ], + "taxes": [ + { + "type": "tax-hst-ns", + "amount": { + "value": "789", + "currency": "CAD" + } + } + ], + "transit_time_days": 3, + "transit_time_not_available": false, + "carrier_name": "Purolator", + "service_name": "Ground" + } + ] +} +""" + +ErrorResponse = """ +{ + "message": "Unable to get rates", + "data": {"services":"invalid-syntax"} +} +""" + +ParsedRateResponse = [ + [ + { + "carrier_id": "freightcom_rest", + "carrier_name": "freightcom_rest", + "currency": "CAD", + "extra_charges": [ + { + "amount": 33.68, + "currency": "CAD", + "name": "Base charge" + }, + { + "amount": 10.01, + "currency": "CAD", + "name": "fuel" + }, + { + "amount": 0.51, + "currency": "CAD", + "name": "carbon-surcharge" + }, + { + "amount": 2.18, + "currency": "CAD", + "name": "residential-delivery" + }, + { + "amount": 6.96, + "currency": "CAD", + "name": "tax-hst-ns" + } + ], + "meta": { + "rate_provider": "canpar", + "service_name": "freightcom_canpar_ground" + }, + "service": "freightcom_canpar_ground", + "total_charge": 53.34, + "transit_days": 2 + }, + { + "carrier_id": "freightcom_rest", + "carrier_name": "freightcom_rest", + "currency": "CAD", + "extra_charges": [ + { + "amount": 40.87, + "currency": "CAD", + "name": "Base charge" + }, + { + "amount": 12.54, + "currency": "CAD", + "name": "fuel" + }, + { + "amount": 2.78, + "currency": "CAD", + "name": "residential-delivery" + }, + { + "amount": 8.43, + "currency": "CAD", + "name": "tax-hst-ns" + } + ], + "meta": { + "rate_provider": "fedex", + "service_name": "freightcom_fedex_ground" + }, + "service": "freightcom_fedex_ground", + "total_charge": 64.62, + "transit_days": 2 + }, + { + "carrier_id": "freightcom_rest", + "carrier_name": "freightcom_rest", + "currency": "CAD", + "extra_charges": [ + { + "amount": 41.34, + "currency": "CAD", + "name": "Base charge" + }, + { + "amount": 11.24, + "currency": "CAD", + "name": "fuel" + }, + { + "amount": 7.89, + "currency": "CAD", + "name": "tax-hst-ns" + } + ], + "meta": { + "rate_provider": "purolator", + "service_name": "freightcom_purolator_ground" + }, + "service": "freightcom_purolator_ground", + "total_charge": 60.47, + "transit_days": 3 + } + ], + [] +] + +ParsedErrorResponse = [ + [], + [ + { + "carrier_id": "freightcom_rest", + "carrier_name": "freightcom_rest", + "message": "Unable to get rates", + "details": { + "services": "invalid-syntax" + } + } + ] +] diff --git a/plugins/freightcom_rest/tests/freightcom_rest/test_shipment.py b/plugins/freightcom_rest/tests/freightcom_rest/test_shipment.py new file mode 100644 index 0000000..81dfc09 --- /dev/null +++ b/plugins/freightcom_rest/tests/freightcom_rest/test_shipment.py @@ -0,0 +1,487 @@ +"""Freightcom Rest carrier shipment tests.""" +import datetime +import unittest +from unittest.mock import patch, ANY +from .fixture import gateway +import logging +import karrio.sdk as karrio +import karrio.lib as lib +import karrio.core.models as models + +logger = logging.getLogger(__name__) + +class TestFreightcomRestShipment(unittest.TestCase): + def setUp(self): + self.maxDiff = None + self.ShipmentRequest = models.ShipmentRequest(**ShipmentPayload) + self.ShipmentCancelRequest = models.ShipmentCancelRequest(**ShipmentCancelPayload) + + def test_create_shipment_request(self): + request = gateway.mapper.create_shipment_request(self.ShipmentRequest) + self.assertEqual(lib.to_dict(request.serialize()), ShipmentRequest) + + def test_create_shipment(self): + with patch("karrio.mappers.freightcom_rest.proxy.lib.request") as mock: + mock.side_effect = [ShipmentFirstResponse, ShipmentResponse] + karrio.Shipment.create(self.ShipmentRequest).from_(gateway) + + self.assertEqual(mock.call_count, 2) + + # Check first API call to create shipment + self.assertEqual( + mock.call_args_list[0][1]["url"], + f"{gateway.settings.server_url}/shipment" + ) + + # Check second API call to get shipment details using the ID + self.assertEqual( + mock.call_args_list[1][1]["url"], + f"{gateway.settings.server_url}/shipment/HNrzG2iRHKJ6XN0CQwYgKBQnABvx2Yi5" + ) + self.assertEqual(mock.call_args_list[1][1]["method"], "GET") + + def test_parse_shipment_response(self): + with patch("karrio.mappers.freightcom_rest.proxy.lib.request") as mock: + mock.side_effect = [ + ShipmentFirstResponse, + ShipmentResponse, + "base64_encoded_label_data", + "base64_encoded_invoice_data" + ] + + parsed_response = ( + karrio.Shipment.create(self.ShipmentRequest) + .from_(gateway) + .parse() + ) + + self.assertEqual(mock.call_count, 4) + + # Check that the result matches the expected parsed response + self.assertListEqual(lib.to_dict(parsed_response), ParsedShipmentResponse) + + def test_create_shipment_cancel_request(self): + request = gateway.mapper.create_cancel_shipment_request(self.ShipmentCancelRequest) + self.assertEqual(request.serialize(), ShipmentCancelRequest) + + def test_cancel_shipment(self): + with patch("karrio.mappers.freightcom_rest.proxy.lib.request") as mock: + mock.return_value = "{}" + karrio.Shipment.cancel(self.ShipmentCancelRequest).from_(gateway) + self.assertEqual( + mock.call_args[1]["url"], + f"{gateway.settings.server_url}/shipment/{self.ShipmentCancelRequest.shipment_identifier}", + ) + + def test_parse_shipment_cancel_response(self): + with patch("karrio.mappers.freightcom_rest.proxy.lib.request") as mock: + mock.return_value = ShipmentCancelResponse + parsed_response = ( + karrio.Shipment.cancel(self.ShipmentCancelRequest) + .from_(gateway) + .parse() + ) + self.assertListEqual(lib.to_dict(parsed_response), ParsedShipmentCancelResponse) + + # def test_parse_error_response(self): + # with patch("karrio.mappers.freightcom_rest.proxy.lib.request") as mock: + # mock.return_value = ErrorResponse + # parsed_response = ( + # karrio.Shipment.create(self.ShipmentRequest) + # .from_(gateway) + # .parse() + # ) + # self.assertListEqual(lib.to_dict(parsed_response), ParsedErrorResponse) + + +if __name__ == "__main__": + unittest.main() + + +ShipmentPayload = { + "shipper": { + "company_name": "Test Company - From", + "address_line1": "9, Van Der Graaf Court", + "city": "Brampton", + "postal_code": "L4T3T1", + "country_code": "CA", + "state_code": "ON", + "email": "shipper@example.com", + "phone_number": "(123) 114 1499" + }, + "recipient": { + "company_name": "Test Company - Destination", + "address_line1": "1410 Fall River Rd", + "city": "Fall River", + "country_code": "CA", + "postal_code": "B2T1J1", + "residential": "true", + "state_code": "NS", + "email": "recipient@example.com", + "phone_number": "(999) 999 9999" + }, + "parcels": [ + { + "height": 50, + "length": 50, + "weight": 20, + "width": 12, + "dimension_unit": "CM", + "weight_unit": "KG", + "description": "Package 1 Description" + }, + { + "height": 30, + "length": 50, + "weight": 20, + "width": 12, + "dimension_unit": "CM", + "weight_unit": "KG", + "description": "Package 2 Description" + } + ], + "service": "freightcom_canpar_ground", + "options": { + "signature_confirmation": True, + "shipping_date": datetime.datetime(2025, 2, 25, 1, 0).strftime("%Y-%m-%dT%H:%M"), + }, + "reference": "#Order 11111", +} + +ShipmentCancelPayload = { + "shipment_identifier": "shipment_id", +} + + +ShipmentRequest = { + "details": { + "destination": { + "address": { + "address_line_1": "1410 Fall River Rd", + "city": "Fall River", + "country": "CA", + "postal_code": "B2T1J1", + "region": "NS", + }, + "email_addresses": ["recipient@example.com"], + 'name': 'Test Company - Destination', + "phone_number": {"number": "(999) 999 9999"}, + "ready_at": { + "hour": 10, "minute": 0 + }, + "ready_until": { + "hour": 17, "minute": 0 + }, + "receives_email_updates": True, + "residential": False, + "signature_requirement": "required" + }, + "origin": { + "address": { + "address_line_1": "9, Van Der Graaf Court", + "city": "Brampton", + "country": "CA", + "postal_code": "L4T3T1", + "region": "ON", + }, + "name": "Test Company - From", + "email_addresses": ["shipper@example.com"], + "phone_number": {"number": "(123) 114 1499"}, + "residential": False + }, + "expected_ship_date": {"day": 25, "month": 2, "year": 2025}, + "packaging_type": "package", + "packaging_properties": { + "packages": [ + { + "description": "Package 1 Description", + "measurements": { + "cuboid": { + "h": 50.0, + "l": 50.0, + "unit": "cm", + "w": 12.0 + }, + "weight": { + "unit": "kg", + "value": 20.0 + } + } + }, + { + "description": "Package 2 Description", + "measurements": { + "cuboid": { + "h": 30.0, + "l": 50.0, + "unit": "cm", + "w": 12.0 + }, + "weight": { + "unit": "kg", + "value": 20.0 + } + } + } + ], + }, + "reference_codes": ["#Order 11111"] + }, + 'payment_method_id': 'string', + "service_id": "canpar.ground", + "unique_id": ANY +} + +ShipmentCancelRequest = "shipment_id" + + +ShipmentResponse = """ +{ +"shipment": { + "id": "uQeh1XwbVIbIyP9mEPtVM2puAFZYmAYA", + "unique_id": "38a8b937-4262-497b-8f5c-b9d9d4c6bae6", + "state": "waiting-for-scheduling", + "transaction_number": "19989362", + "primary_tracking_number": "1ZXXXXXXXXXXXXXXXX", + "tracking_numbers": [ + "1ZXXXXXXXXXXXXXXXX", + "1ZXXXXXXXXXXXXXXXX" + ], + "tracking_url": "https://www.ups.com/WebTracking?trackingNumber=1ZXXXXXXXXXXXXXXXX", + "return_tracking_number": "", + "bolnumber": "", + "pickup_confirmation_number": "", + "details": { + "id": "HNrzG2iRHKJ6XN0CQwYgKBQnABvx2Yi5", + "expected_ship_date": { + "year": 2025, + "month": 2, + "day": 12 + }, + "origin": { + "searchable_id": "", + "name": "Cheques Plus", + "address": { + "address_line1": "4054 Rue Alfred Laliberté", + "address_line2": "", + "unit_number": "", + "city": "Boisbriand", + "region": "QC", + "country": "CA", + "postal_code": "J7H 1P8", + "validated": false + }, + "residential": false, + "business_type": "", + "tailgate_required": false, + "instructions": "", + "contact_name": "Shipping", + "phone_number": { + "number": "+1 450-323-6247", + "extension": "" + }, + "email_addresses": [ + "sales@chequesplus.com" + ], + "receives_email_updates": false, + "address_book_contact_id": "" + }, + "destination": { + "searchable_id": "", + "name": "ASAP Cheques", + "address": { + "address_line1": "623 Fortune Crescent #100", + "address_line2": "", + "unit_number": "", + "city": "Kingston", + "region": "ON", + "country": "CA", + "postal_code": "K7P 0L5", + "validated": false + }, + "residential": false, + "business_type": "", + "tailgate_required": false, + "instructions": "", + "contact_name": "ASAP Cheques Kingston", + "phone_number": { + "number": "+1 888-324-3783", + "extension": "" + }, + "email_addresses": [ + "admin@shipngo.ca" + ], + "receives_email_updates": false, + "address_book_contact_id": "", + "ready_at": { + "hour": 10, + "minute": 0, + "populated": true + }, + "ready_until": { + "hour": 17, + "minute": 0, + "populated": true + }, + "signature_requirement": "not-required" + }, + "alternate_destination": null, + "reference_codes": [ + "ss" + ], + "packaging_type": "package", + "packaging_properties": { + "packages": [ + { + "measurements": { + "cuboid": { + "l": 10, + "w": 20, + "h": 18.2, + "unit": "cm" + }, + "weight": { + "value": 1, + "unit": "kg" + } + }, + "description": "N/A", + "special_handling_required": false + }, + { + "measurements": { + "cuboid": { + "l": 10, + "w": 33.7, + "h": 18.2, + "unit": "cm" + }, + "weight": { + "value": 1, + "unit": "kg" + } + }, + "description": "N/A", + "special_handling_required": false + } + ], + "includes_return_label": false + }, + "insurance": null, + "billing_contact": null + }, + "transport_data": null, + "labels": [ + { + "size": "letter", + "format": "pdf", + "url": "https://s3.us-east-2.amazonaws.com/ssd-test-external/labels/uQeh1XwbVIbIyP9mEPtVM2puAFZYmAYA/yRWNRmUkCGMKIZOjMHNUSy9JlXPYjvVb/shipping-label-19989362-letter.pdf", + "padded": false + }, + { + "size": "a6", + "format": "zpl", + "url": "https://s3.us-east-2.amazonaws.com/ssd-test-external/labels/uQeh1XwbVIbIyP9mEPtVM2puAFZYmAYA/yRWNRmUkCGMKIZOjMHNUSy9JlXPYjvVb/shipping-label-19989362-a6.zpl", + "padded": false + }, + { + "size": "a6", + "format": "pdf", + "url": "https://s3.us-east-2.amazonaws.com/ssd-test-external/labels/uQeh1XwbVIbIyP9mEPtVM2puAFZYmAYA/yRWNRmUkCGMKIZOjMHNUSy9JlXPYjvVb/shipping-label-19989362-a6.pdf", + "padded": false + }, + { + "size": "a6", + "format": "pdf", + "url": "https://s3.us-east-2.amazonaws.com/ssd-test-external/labels/uQeh1XwbVIbIyP9mEPtVM2puAFZYmAYA/yRWNRmUkCGMKIZOjMHNUSy9JlXPYjvVb/shipping-label-19989362-a6-w-padding.pdf", + "padded": true + } + ], + "customs_invoice_url": "https://s3.us-east-2.amazonaws.com/ssd-test-external/labels/uQeh1XwbVIbIyP9mEPtVM2puAFZYmAYA/yRWNRmUkCGMKIZOjMHNUSy9JlXPYjvVb/shipping-label-19989362-a6-w-padding.pdf", + "rate": { + "service_id": "ups.standard", + "valid_until": { + "year": 2025, + "month": 2, + "day": 20 + }, + "total": { + "value": "1779", + "currency": "CAD" + }, + "base": { + "value": "1779", + "currency": "CAD" + }, + "surcharges": [], + "taxes": [], + "transit_time_days": 1, + "transit_time_not_available": false, + "carrier_name": "UPS", + "service_name": "Standard" + }, + "order_source": "Api" + } +} +""" + +ShipmentCancelResponse = """{} +""" + +ErrorResponse = """{ + "message": "Unable to get rates", + "data": {"services":"invalid-syntax"} +}""" + +ParsedShipmentResponse = [ + { + "carrier_id": "freightcom_rest", + "carrier_name": "freightcom_rest", + "docs": { + "invoice": "base64_encoded_invoice_data", + "label": "base64_encoded_label_data", + }, + "label_type": "PDF", + "meta": { + "carrier_tracking_link": "https://www.ups.com/WebTracking?trackingNumber=1ZXXXXXXXXXXXXXXXX", + "freightcom_service_id": "ups.standard", + "freightcom_shipment_identifier": "uQeh1XwbVIbIyP9mEPtVM2puAFZYmAYA", + "freightcom_unique_id": "38a8b937-4262-497b-8f5c-b9d9d4c6bae6", + "rate_provider": "ups", + "service_name": "freightcom_ups_standard", + "tracking_numbers": ["1ZXXXXXXXXXXXXXXXX"] + }, + "shipment_identifier": "uQeh1XwbVIbIyP9mEPtVM2puAFZYmAYA", + "tracking_number": "1ZXXXXXXXXXXXXXXXX", + }, + [] +] + +ParsedShipmentCancelResponse = [ + { + "carrier_id": "freightcom_rest", + "carrier_name": "freightcom_rest", + "success": True, + "operation": "Cancel Shipment" + }, + [] +] + +ParsedErrorResponse = [ + [], + [ + { + "carrier_id": "freightcom_rest", + "carrier_name": "freightcom_rest", + "message": "Unable to get rates", + "details": { + "services": "invalid-syntax" + } + } + ] +] + + +ShipmentFirstResponse = """ +{"id": "HNrzG2iRHKJ6XN0CQwYgKBQnABvx2Yi5"} +""" From 1e5157f6a36e2c0b6f9b97db1f03c21cc4d141e8 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Wed, 13 Aug 2025 15:25:54 -0400 Subject: [PATCH 02/23] fix: include metadata.json in freightcom_rest package distribution - Add package-data configuration to ensure metadata.json is included when the package is built and installed - This fixes the missing file error during package installation --- plugins/freightcom_rest/pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/freightcom_rest/pyproject.toml b/plugins/freightcom_rest/pyproject.toml index 8cb9cb3..f02b043 100644 --- a/plugins/freightcom_rest/pyproject.toml +++ b/plugins/freightcom_rest/pyproject.toml @@ -36,4 +36,7 @@ include-package-data = true [tool.setuptools.packages.find] exclude = ["tests.*", "tests"] -namespaces = true \ No newline at end of file +namespaces = true + +[tool.setuptools.package-data] +"karrio.providers.freightcom_rest" = ["metadata.json"] From 755b5b29f497a8cce4386b56caf9c70d0ae371ce Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Wed, 13 Aug 2025 18:30:47 -0400 Subject: [PATCH 03/23] fix: correct unit price value calculation in Freightcom shipment creation --- .../karrio/providers/freightcom_rest/shipment/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index 7405880..24bd292 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -343,7 +343,7 @@ def shipment_request( num_units=item.quantity, unit_price=freightcom_rest_req.TotalCostType( currency=item.value_currency, - value=str(item.value_amount) + value=str(int(item.value_amount * 100)) ), description=item.description ) for item in customs.commodities From 85b393b374eb85709e94fbd5d863cd0430f59c8d Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Wed, 13 Aug 2025 18:31:10 -0400 Subject: [PATCH 04/23] fix: handle empty response in Freightcom shipment creation - Add failsafe handling for empty responses to prevent errors in shipment ID extraction. - Ensure deserialization fallback to empty JSON if response is missing. --- .../freightcom_rest/karrio/mappers/freightcom_rest/proxy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/freightcom_rest/karrio/mappers/freightcom_rest/proxy.py b/plugins/freightcom_rest/karrio/mappers/freightcom_rest/proxy.py index 672f46a..fff812e 100644 --- a/plugins/freightcom_rest/karrio/mappers/freightcom_rest/proxy.py +++ b/plugins/freightcom_rest/karrio/mappers/freightcom_rest/proxy.py @@ -51,11 +51,11 @@ def create_shipment(self, request: lib.Serializable) -> lib.Deserializable: response = self._send_request( path="/shipment", request=lib.Serializable(request.value, lib.to_json) ) + response_dict = lib.failsafe(lambda: lib.to_dict(response)) or {} + shipment_id = response_dict.get('id') - shipment_id = lib.to_dict(response).get('id') if not shipment_id: - return lib.Deserializable(response, lib.to_dict) - + return lib.Deserializable(response if response else "{}", lib.to_dict) # Step 2: retry because api return empty bytes if done to fast time.sleep(1) From db771b74d538de3b117e040cb612546a6a7a56d0 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Wed, 13 Aug 2025 18:42:10 -0400 Subject: [PATCH 05/23] fix: update duty tax recipient type and commodity value formatting in Freightcom shipment creation - Set duty tax recipient type to "receiver" as required by the API. - Adjust commodity value calculation to ensure formatting meets API expectations. --- .../providers/freightcom_rest/shipment/create.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index 24bd292..a5226c2 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -343,16 +343,19 @@ def shipment_request( num_units=item.quantity, unit_price=freightcom_rest_req.TotalCostType( currency=item.value_currency, + # the api expect to be a whole number like 16900 for 169.00 value=str(int(item.value_amount * 100)) ), description=item.description ) for item in customs.commodities ], tax_recipient=freightcom_rest_req.TaxRecipientType( - type=provider_units.PaymentType.map( - customs.duty.paid_by - ).value - or "shipper", + # they require it to be receiver + # type=provider_units.PaymentType.map( + # customs.duty.paid_by + # ).value + # or "shipper", + type = "receiver", name=customs.duty_billing_address.company_name or customs.duty.person_name, address=freightcom_rest_req.AddressType( address_line_1=customs.duty_billing_address.address_line1, From d16b8b82acf864665c340419f94a3f43d14bfc5e Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Mon, 18 Aug 2025 17:09:26 -0400 Subject: [PATCH 06/23] fix: update duty tax recipient mapping in Freightcom shipment creation - Map duty paid_by field to API-compatible recipient type with a fallback to "receiver". --- .../providers/freightcom_rest/shipment/create.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index a5226c2..5d55b04 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -350,12 +350,11 @@ def shipment_request( ) for item in customs.commodities ], tax_recipient=freightcom_rest_req.TaxRecipientType( - # they require it to be receiver - # type=provider_units.PaymentType.map( - # customs.duty.paid_by - # ).value - # or "shipper", - type = "receiver", + # they require it to be receiver, has to be enabled in the api + type=provider_units.PaymentType.map( + customs.duty.paid_by + ).value + or "receiver", name=customs.duty_billing_address.company_name or customs.duty.person_name, address=freightcom_rest_req.AddressType( address_line_1=customs.duty_billing_address.address_line1, From a4022b5c879ace695a87f2b113f06baf3dc55bda Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Mon, 24 Nov 2025 13:52:03 -0500 Subject: [PATCH 07/23] fix: enhance error message formatting in Freightcom error handling - Update error message construction to include details or data values if available, improving clarity in error responses. --- .../karrio/providers/freightcom_rest/error.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py index 7bff4d3..98405e8 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py @@ -21,7 +21,11 @@ def parse_error_response( models.Message( carrier_id=settings.carrier_id, carrier_name=settings.carrier_name, - message=error.get("message"), + message=( + error.get("message") + ": " + "; ".join((error.get("details", {}) or error.get("data", {})).values()) + if (error.get("details", {}) or error.get("data", {})) + else error.get("message") + ), details={ **kwargs, **(error.get('data', {})) From 0e5c80797348a0945d3f84b9f7151dba8b25ebde Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Mon, 24 Nov 2025 15:08:21 -0500 Subject: [PATCH 08/23] fix: improve error message detail extraction in Freightcom error handling - Refactor error message construction to format details and data values more clearly, enhancing the readability of error responses. --- .../freightcom_rest/karrio/providers/freightcom_rest/error.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py index 98405e8..e37c3d0 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py @@ -22,7 +22,7 @@ def parse_error_response( carrier_id=settings.carrier_id, carrier_name=settings.carrier_name, message=( - error.get("message") + ": " + "; ".join((error.get("details", {}) or error.get("data", {})).values()) + error.get("message") + ": " + "; ".join(f"{k.replace('details.', '')}: {v}" for k, v in (error.get("details", {}) or error.get("data", {})).items()) if (error.get("details", {}) or error.get("data", {})) else error.get("message") ), From 8190aa9f33610d668bac53a6cc48efb3d6c5b996 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Mon, 24 Nov 2025 16:16:15 -0500 Subject: [PATCH 09/23] feat: update Freightcom schemas to support customs data and paperless documents - Add fields for customs data, product composition, and paperless customs documentation. - Align schemas with Freightcom API Version 2.6.2 changes. --- .../karrio/providers/freightcom_rest/rate.py | 41 +++++++++++- .../freightcom_rest/shipment/create.py | 30 ++++++++- .../karrio/providers/freightcom_rest/units.py | 1 + .../schemas/freightcom_rest/rate_request.py | 41 ++++++++++-- .../schemas/freightcom_rest/rate_response.py | 11 ++++ .../freightcom_rest/shipment_request.py | 30 +++++++++ .../freightcom_rest/shipment_response.py | 62 +++++++++++++++++-- .../freightcom_rest/schemas/rate_request.json | 29 ++++++++- .../schemas/rate_response.json | 20 +++++- .../schemas/shipment_request.json | 52 ++++++++++++++-- .../schemas/shipment_response.json | 58 +++++++++++++++-- 11 files changed, 346 insertions(+), 29 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py index b191558..9d33b3b 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py @@ -70,7 +70,6 @@ def _extract_details( *((tax.type, tax.amount.value, tax.amount.currency) for tax in rate.taxes), ] - return models.RateDetails( carrier_id=settings.carrier_id, carrier_name=settings.carrier_name, @@ -90,7 +89,11 @@ def _extract_details( meta=dict( service_name=service_name, rate_provider=rate_provider, - # Add any other useful metadata from the carrier response + request_guaranteed_customs_charges=( + rate.customs_charge_data.is_rate_guaranteed + if hasattr(rate, 'customs_charge_data') and rate.customs_charge_data and hasattr(rate.customs_charge_data, 'is_rate_guaranteed') + else None + ), ), ) @@ -119,6 +122,13 @@ def rate_request( ) # Create the carrier-specific request object + is_intl = shipper.country_code != recipient.country_code + customs = lib.to_customs_info( + payload.customs, + shipper=payload.shipper, + recipient=payload.recipient, + weight_unit=packages.weight_unit, + ) if payload.customs else None packaging_type = provider_units.PackagingType.map(packages.package_type or "small_box").value ship_datetime = lib.to_next_business_datetime( @@ -246,7 +256,32 @@ def rate_request( ) ), reference_codes=[payload.reference] if any(payload.reference or "") else [] - ) + ), + customs_data=( + freightcom_rest_req.CustomsDataType( + products=[ + freightcom_rest_req.ProductType( + product_name=item.description, + weight=freightcom_rest_req.WeightType( + unit="kg" if item.weight_unit.upper() == "KG" else "lb", + value=lib.to_decimal(item.weight) + ), + hs_code=item.hs_code, + country_of_origin=item.origin_country, + num_units=item.quantity, + unit_price=freightcom_rest_req.TotalCostType( + currency=item.value_currency, + value=str(int(item.value_amount * 100)) + ), + description=item.description, + fda_regulated="no" + ) for item in customs.commodities + ] if customs and customs.commodities else [], + request_guaranteed_customs_charges=options.request_guaranteed_customs_charges.state if hasattr(options, 'request_guaranteed_customs_charges') else None + ) + if is_intl and customs and customs.commodities + else None + ), ) return lib.Serializable(request, lib.to_dict) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index 5d55b04..7e9b723 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -322,7 +322,32 @@ def shipment_request( ) for parcel in packages ] if packaging_type == "courier-pak" else [], ), - reference_codes=[payload.reference] if payload.reference else [] + reference_codes=[payload.reference] if payload.reference else [], + customs_data=( + freightcom_rest_req.CustomsDataType( + products=[ + freightcom_rest_req.ProductType( + product_name=item.description, + weight=freightcom_rest_req.WeightType( + unit="kg" if item.weight_unit.upper() == "KG" else "lb", + value=lib.to_decimal(item.weight) + ), + hs_code=item.hs_code, + country_of_origin=item.origin_country, + num_units=item.quantity, + unit_price=freightcom_rest_req.TotalCostType( + currency=item.value_currency, + value=str(int(item.value_amount * 100)) + ), + description=item.description, + fda_regulated="no" + ) for item in customs.commodities + ] if customs and customs.commodities else [], + request_guaranteed_customs_charges=options.request_guaranteed_customs_charges.state if hasattr(options, 'request_guaranteed_customs_charges') else None + ) + if is_intl and customs and customs.commodities + else None + ), ), customs_invoice=( freightcom_rest_req.CustomsInvoiceType( @@ -346,7 +371,8 @@ def shipment_request( # the api expect to be a whole number like 16900 for 169.00 value=str(int(item.value_amount * 100)) ), - description=item.description + description=item.description, + fda_regulated="no" ) for item in customs.commodities ], tax_recipient=freightcom_rest_req.TaxRecipientType( diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py index cc21ec2..77ccc1f 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py @@ -91,6 +91,7 @@ class ShippingOption(lib.Enum): freightcom_dangerous_goods_type = lib.OptionEnum("dangerousGoodsType", bool) freightcom_stackable = lib.OptionEnum("stackable", bool) freightcom_payment_method = lib.OptionEnum("payment_method", str) + freightcom_request_guaranteed_customs_charges = lib.OptionEnum("request_guaranteed_customs_charges", bool) """ Unified Option type mapping """ # saturday_delivery = freightcom_saturday_pickup_required diff --git a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_request.py b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_request.py index c8c1b2a..5fafdc5 100644 --- a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_request.py +++ b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_request.py @@ -3,6 +3,40 @@ import typing +@attr.s(auto_attribs=True) +class ProductCompositionAllocationType: + provided: typing.Optional[bool] = None + steel_percentage: typing.Optional[int] = None + aluminum_percentage: typing.Optional[int] = None + copper_percentage: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class TotalCostType: + currency: typing.Optional[str] = None + value: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class ProductType: + hs_code: typing.Optional[str] = None + country_of_origin: typing.Optional[str] = None + num_units: typing.Optional[int] = None + unit_price: typing.Optional[TotalCostType] = jstruct.JStruct[TotalCostType] + description: typing.Optional[str] = None + cusma_included: typing.Optional[bool] = None + non_auto_parts: typing.Optional[bool] = None + fda_regulated: typing.Optional[str] = None + product_composition_allocation: typing.Optional[ProductCompositionAllocationType] = jstruct.JStruct[ProductCompositionAllocationType] + product_composition_allocation_zero: typing.Optional[bool] = None + + +@attr.s(auto_attribs=True) +class CustomsDataType: + products: typing.Optional[typing.List[ProductType]] = jstruct.JList[ProductType] + request_guaranteed_customs_charges: typing.Optional[bool] = None + + @attr.s(auto_attribs=True) class AddressType: address_line_1: typing.Optional[str] = None @@ -76,12 +110,6 @@ class DangerousGoodsDetailsType: emergency_contact_phone_number: typing.Optional[PhoneNumberType] = jstruct.JStruct[PhoneNumberType] -@attr.s(auto_attribs=True) -class TotalCostType: - currency: typing.Optional[str] = None - value: typing.Optional[int] = None - - @attr.s(auto_attribs=True) class InsuranceType: type: typing.Optional[str] = None @@ -164,6 +192,7 @@ class DetailsType: packaging_type: typing.Optional[str] = None packaging_properties: typing.Optional[PackagingPropertiesType] = jstruct.JStruct[PackagingPropertiesType] reference_codes: typing.Optional[typing.List[str]] = None + customs_data: typing.Optional[CustomsDataType] = jstruct.JStruct[CustomsDataType] @attr.s(auto_attribs=True) diff --git a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_response.py b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_response.py index 964ba1b..dcab724 100644 --- a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_response.py +++ b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/rate_response.py @@ -9,6 +9,15 @@ class BaseType: value: typing.Optional[int] = None +@attr.s(auto_attribs=True) +class CustomsChargeDataType: + duties_and_taxes_surcharge_keys: typing.Optional[typing.List[str]] = None + guarantee_fee_surcharge_keys: typing.Optional[typing.List[str]] = None + carrier_and_government_fees_surcharge_keys: typing.Optional[typing.List[str]] = None + processing_fees_surcharge_keys: typing.Optional[typing.List[str]] = None + is_rate_guaranteed: typing.Optional[bool] = None + + @attr.s(auto_attribs=True) class SurchargeType: type: typing.Optional[str] = None @@ -34,6 +43,8 @@ class RateType: taxes: typing.Optional[typing.List[SurchargeType]] = jstruct.JList[SurchargeType] transit_time_days: typing.Optional[int] = None transit_time_not_available: typing.Optional[bool] = None + paperless: typing.Optional[bool] = None + customs_charge_data: typing.Optional[CustomsChargeDataType] = jstruct.JStruct[CustomsChargeDataType] @attr.s(auto_attribs=True) diff --git a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_request.py b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_request.py index ce35f92..9ce9658 100644 --- a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_request.py +++ b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_request.py @@ -21,6 +21,14 @@ class BrokerType: fda_number: typing.Optional[str] = None +@attr.s(auto_attribs=True) +class ProductCompositionAllocationType: + provided: typing.Optional[bool] = None + steel_percentage: typing.Optional[int] = None + aluminum_percentage: typing.Optional[int] = None + copper_percentage: typing.Optional[int] = None + + @attr.s(auto_attribs=True) class TotalCostType: currency: typing.Optional[str] = None @@ -42,6 +50,11 @@ class ProductType: num_units: typing.Optional[int] = None unit_price: typing.Optional[TotalCostType] = jstruct.JStruct[TotalCostType] description: typing.Optional[str] = None + cusma_included: typing.Optional[bool] = None + non_auto_parts: typing.Optional[bool] = None + fda_regulated: typing.Optional[str] = None + product_composition_allocation: typing.Optional[ProductCompositionAllocationType] = jstruct.JStruct[ProductCompositionAllocationType] + product_composition_allocation_zero: typing.Optional[bool] = None @attr.s(auto_attribs=True) @@ -83,6 +96,12 @@ class CustomsInvoiceType: details: typing.Optional[CustomsInvoiceDetailsType] = jstruct.JStruct[CustomsInvoiceDetailsType] +@attr.s(auto_attribs=True) +class CustomsDataType: + products: typing.Optional[typing.List[ProductType]] = jstruct.JList[ProductType] + request_guaranteed_customs_charges: typing.Optional[bool] = None + + @attr.s(auto_attribs=True) class ReadyType: hour: typing.Optional[int] = None @@ -215,6 +234,7 @@ class ShipmentRequestDetailsType: packaging_properties: typing.Optional[PackagingPropertiesType] = jstruct.JStruct[PackagingPropertiesType] insurance: typing.Optional[InsuranceType] = jstruct.JStruct[InsuranceType] reference_codes: typing.Optional[typing.List[str]] = None + customs_data: typing.Optional[CustomsDataType] = jstruct.JStruct[CustomsDataType] @attr.s(auto_attribs=True) @@ -224,6 +244,14 @@ class DispatchDetailsType: ready_until: typing.Optional[ReadyType] = jstruct.JStruct[ReadyType] +@attr.s(auto_attribs=True) +class PaperlessCustomsDocumentType: + type: typing.Optional[str] = None + type_other_name: typing.Optional[str] = None + file_name: typing.Optional[str] = None + file_base64: typing.Optional[str] = None + + @attr.s(auto_attribs=True) class PickupDetailsType: pre_scheduled_pickup: typing.Optional[bool] = None @@ -239,8 +267,10 @@ class PickupDetailsType: class ShipmentRequestType: unique_id: typing.Optional[str] = None payment_method_id: typing.Optional[str] = None + customs_and_duties_payment_method_id: typing.Optional[str] = None service_id: typing.Optional[str] = None details: typing.Optional[ShipmentRequestDetailsType] = jstruct.JStruct[ShipmentRequestDetailsType] customs_invoice: typing.Optional[CustomsInvoiceType] = jstruct.JStruct[CustomsInvoiceType] pickup_details: typing.Optional[PickupDetailsType] = jstruct.JStruct[PickupDetailsType] dispatch_details: typing.Optional[DispatchDetailsType] = jstruct.JStruct[DispatchDetailsType] + paperless_customs_documents: typing.Optional[typing.List[PaperlessCustomsDocumentType]] = jstruct.JList[PaperlessCustomsDocumentType] diff --git a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_response.py b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_response.py index 2e57baf..60f9315 100644 --- a/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_response.py +++ b/plugins/freightcom_rest/karrio/schemas/freightcom_rest/shipment_response.py @@ -3,6 +3,40 @@ import typing +@attr.s(auto_attribs=True) +class ProductCompositionAllocationType: + provided: typing.Optional[bool] = None + steel_percentage: typing.Optional[int] = None + aluminum_percentage: typing.Optional[int] = None + copper_percentage: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class BaseType: + currency: typing.Optional[str] = None + value: typing.Optional[int] = None + + +@attr.s(auto_attribs=True) +class ProductType: + hs_code: typing.Optional[str] = None + country_of_origin: typing.Optional[str] = None + num_units: typing.Optional[int] = None + unit_price: typing.Optional[BaseType] = jstruct.JStruct[BaseType] + description: typing.Optional[str] = None + cusma_included: typing.Optional[bool] = None + non_auto_parts: typing.Optional[bool] = None + fda_regulated: typing.Optional[str] = None + product_composition_allocation: typing.Optional[ProductCompositionAllocationType] = jstruct.JStruct[ProductCompositionAllocationType] + product_composition_allocation_zero: typing.Optional[bool] = None + + +@attr.s(auto_attribs=True) +class CustomsDataType: + products: typing.Optional[typing.List[ProductType]] = jstruct.JList[ProductType] + request_guaranteed_customs_charges: typing.Optional[bool] = None + + @attr.s(auto_attribs=True) class AddressType: address_line_1: typing.Optional[str] = None @@ -49,12 +83,6 @@ class ExpectedShipDateType: day: typing.Optional[int] = None -@attr.s(auto_attribs=True) -class BaseType: - currency: typing.Optional[str] = None - value: typing.Optional[int] = None - - @attr.s(auto_attribs=True) class InsuranceType: type: typing.Optional[str] = None @@ -164,6 +192,7 @@ class DetailsType: packaging_properties: typing.Optional[PackagingPropertiesType] = jstruct.JStruct[PackagingPropertiesType] insurance: typing.Optional[InsuranceType] = jstruct.JStruct[InsuranceType] reference_codes: typing.Optional[typing.List[str]] = None + customs_data: typing.Optional[CustomsDataType] = jstruct.JStruct[CustomsDataType] @attr.s(auto_attribs=True) @@ -174,6 +203,24 @@ class LabelType: padded: typing.Optional[bool] = None +@attr.s(auto_attribs=True) +class PaperlessCustomsDocumentType: + id: typing.Optional[str] = None + type: typing.Optional[str] = None + type_other_name: typing.Optional[str] = None + file_name: typing.Optional[str] = None + url: typing.Optional[str] = None + + +@attr.s(auto_attribs=True) +class CustomsChargeDataType: + duties_and_taxes_surcharge_keys: typing.Optional[typing.List[str]] = None + guarantee_fee_surcharge_keys: typing.Optional[typing.List[str]] = None + carrier_and_government_fees_surcharge_keys: typing.Optional[typing.List[str]] = None + processing_fees_surcharge_keys: typing.Optional[typing.List[str]] = None + is_rate_guaranteed: typing.Optional[bool] = None + + @attr.s(auto_attribs=True) class SurchargeType: type: typing.Optional[str] = None @@ -192,6 +239,8 @@ class RateType: taxes: typing.Optional[typing.List[SurchargeType]] = jstruct.JList[SurchargeType] transit_time_days: typing.Optional[int] = None transit_time_not_available: typing.Optional[bool] = None + paperless: typing.Optional[bool] = None + customs_charge_data: typing.Optional[CustomsChargeDataType] = jstruct.JStruct[CustomsChargeDataType] @attr.s(auto_attribs=True) @@ -217,6 +266,7 @@ class ShipmentType: customs_invoice_url: typing.Optional[str] = None rate: typing.Optional[RateType] = jstruct.JStruct[RateType] order_source: typing.Optional[str] = None + paperless_customs_documents: typing.Optional[typing.List[PaperlessCustomsDocumentType]] = jstruct.JList[PaperlessCustomsDocumentType] @attr.s(auto_attribs=True) diff --git a/plugins/freightcom_rest/schemas/rate_request.json b/plugins/freightcom_rest/schemas/rate_request.json index 55b90a3..78083c0 100644 --- a/plugins/freightcom_rest/schemas/rate_request.json +++ b/plugins/freightcom_rest/schemas/rate_request.json @@ -166,6 +166,31 @@ }, "reference_codes": [ "string" - ] + ], + "customs_data": { + "products": [ + { + "hs_code": "string", + "country_of_origin": "CA", + "num_units": 1, + "unit_price": { + "currency": "CAD", + "value": "4250" + }, + "description": "string", + "cusma_included": true, + "non_auto_parts": true, + "fda_regulated": "yes", + "product_composition_allocation": { + "provided": true, + "steel_percentage": 1, + "aluminum_percentage": 1, + "copper_percentage": 1 + }, + "product_composition_allocation_zero": true + } + ], + "request_guaranteed_customs_charges": true + } } -} +} \ No newline at end of file diff --git a/plugins/freightcom_rest/schemas/rate_response.json b/plugins/freightcom_rest/schemas/rate_response.json index db82e35..821cf86 100644 --- a/plugins/freightcom_rest/schemas/rate_response.json +++ b/plugins/freightcom_rest/schemas/rate_response.json @@ -41,7 +41,23 @@ } ], "transit_time_days": 5, - "transit_time_not_available": true + "transit_time_not_available": true, + "paperless": true, + "customs_charge_data": { + "duties_and_taxes_surcharge_keys": [ + "string" + ], + "guarantee_fee_surcharge_keys": [ + "string" + ], + "carrier_and_government_fees_surcharge_keys": [ + "string" + ], + "processing_fees_surcharge_keys": [ + "string" + ], + "is_rate_guaranteed": true + } } ] -} +} \ No newline at end of file diff --git a/plugins/freightcom_rest/schemas/shipment_request.json b/plugins/freightcom_rest/schemas/shipment_request.json index 15a543f..d978da1 100644 --- a/plugins/freightcom_rest/schemas/shipment_request.json +++ b/plugins/freightcom_rest/schemas/shipment_request.json @@ -1,6 +1,7 @@ { "unique_id": "string", "payment_method_id": "string", + "customs_and_duties_payment_method_id": "string", "service_id": "string", "details": { "origin": { @@ -163,7 +164,32 @@ }, "reference_codes": [ "string" - ] + ], + "customs_data": { + "products": [ + { + "hs_code": "string", + "country_of_origin": "CA", + "num_units": 1, + "unit_price": { + "currency": "CAD", + "value": "4250" + }, + "description": "string", + "cusma_included": true, + "non_auto_parts": true, + "fda_regulated": "yes", + "product_composition_allocation": { + "provided": true, + "steel_percentage": 1, + "aluminum_percentage": 1, + "copper_percentage": 1 + }, + "product_composition_allocation_zero": true + } + ], + "request_guaranteed_customs_charges": true + } }, "customs_invoice": { "source": "details", @@ -222,7 +248,17 @@ "currency": "CAD", "value": "4250" }, - "description": "string" + "description": "string", + "cusma_included": true, + "non_auto_parts": true, + "fda_regulated": "yes", + "product_composition_allocation": { + "provided": true, + "steel_percentage": 1, + "aluminum_percentage": 1, + "copper_percentage": 1 + }, + "product_composition_allocation_zero": true } ] } @@ -263,5 +299,13 @@ "hour": 15, "minute": 6 } - } -} + }, + "paperless_customs_documents": [ + { + "type": "cusma-form", + "type_other_name": "string", + "file_name": "string", + "file_base64": "string" + } + ] +} \ No newline at end of file diff --git a/plugins/freightcom_rest/schemas/shipment_response.json b/plugins/freightcom_rest/schemas/shipment_response.json index 7a32f87..ab334fc 100644 --- a/plugins/freightcom_rest/schemas/shipment_response.json +++ b/plugins/freightcom_rest/schemas/shipment_response.json @@ -173,7 +173,32 @@ }, "reference_codes": [ "string" - ] + ], + "customs_data": { + "products": [ + { + "hs_code": "string", + "country_of_origin": "CA", + "num_units": 1, + "unit_price": { + "currency": "CAD", + "value": "4250" + }, + "description": "string", + "cusma_included": true, + "non_auto_parts": true, + "fda_regulated": "yes", + "product_composition_allocation": { + "provided": true, + "steel_percentage": 1, + "aluminum_percentage": 1, + "copper_percentage": 1 + }, + "product_composition_allocation_zero": true + } + ], + "request_guaranteed_customs_charges": true + } }, "transport_data": {}, "labels": [ @@ -221,8 +246,33 @@ } ], "transit_time_days": 5, - "transit_time_not_available": true + "transit_time_not_available": true, + "paperless": true, + "customs_charge_data": { + "duties_and_taxes_surcharge_keys": [ + "string" + ], + "guarantee_fee_surcharge_keys": [ + "string" + ], + "carrier_and_government_fees_surcharge_keys": [ + "string" + ], + "processing_fees_surcharge_keys": [ + "string" + ], + "is_rate_guaranteed": true + } }, - "order_source": "string" + "order_source": "string", + "paperless_customs_documents": [ + { + "id": "string", + "type": "string", + "type_other_name": "string", + "file_name": "string", + "url": "string" + } + ] } -} +} \ No newline at end of file From cd2058c9494846a9ecd16612d81ff4b4c5c20b49 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Mon, 24 Nov 2025 16:45:08 -0500 Subject: [PATCH 10/23] fix: refine customs handling for CA to US shipments in rate and shipment requests - Introduce checks for Canadian to US shipments to ensure proper customs processing. - Update conditions to include packaging type validation for customs commodities in both rate and shipment request functions. --- .../freightcom_rest/karrio/providers/freightcom_rest/rate.py | 4 +++- .../karrio/providers/freightcom_rest/shipment/create.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py index 9d33b3b..20ac679 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py @@ -123,6 +123,7 @@ def rate_request( # Create the carrier-specific request object is_intl = shipper.country_code != recipient.country_code + is_ca_to_us = shipper.country_code == "CA" and recipient.country_code == "US" customs = lib.to_customs_info( payload.customs, shipper=payload.shipper, @@ -131,6 +132,7 @@ def rate_request( ) if payload.customs else None packaging_type = provider_units.PackagingType.map(packages.package_type or "small_box").value + is_package_or_courierpak = packaging_type in ["package", "courier-pak"] ship_datetime = lib.to_next_business_datetime( options.shipping_date.state or datetime.datetime.now(), current_format="%Y-%m-%dT%H:%M", @@ -279,7 +281,7 @@ def rate_request( ] if customs and customs.commodities else [], request_guaranteed_customs_charges=options.request_guaranteed_customs_charges.state if hasattr(options, 'request_guaranteed_customs_charges') else None ) - if is_intl and customs and customs.commodities + if is_ca_to_us and is_package_or_courierpak and customs and customs.commodities else None ), ) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index 7e9b723..8614d33 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -173,6 +173,7 @@ def shipment_request( packaging_type = provider_units.PackagingType.map( packages.package_type or "small_box" ).value + is_package_or_courierpak = packaging_type in ["package", "courier-pak"] ship_datetime = lib.to_next_business_datetime( options.shipping_date.state or datetime.datetime.now(), @@ -180,6 +181,7 @@ def shipment_request( ) is_intl = shipper.country_code != recipient.country_code + is_ca_to_us = shipper.country_code == "CA" and recipient.country_code == "US" customs = lib.to_customs_info( payload.customs, shipper=payload.shipper, @@ -345,7 +347,7 @@ def shipment_request( ] if customs and customs.commodities else [], request_guaranteed_customs_charges=options.request_guaranteed_customs_charges.state if hasattr(options, 'request_guaranteed_customs_charges') else None ) - if is_intl and customs and customs.commodities + if is_ca_to_us and is_package_or_courierpak and customs and customs.commodities else None ), ), From 9bfbab9467c282648281fe0ec67eb0393d9c0280 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Mon, 24 Nov 2025 16:53:39 -0500 Subject: [PATCH 11/23] fix: streamline customs data handling in rate request for CA to US shipments - Refactor customs data construction to improve clarity and maintainability. - Ensure proper inclusion of product details while maintaining existing functionality for customs processing. --- .../karrio/providers/freightcom_rest/rate.py | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py index 20ac679..442cf12 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py @@ -257,32 +257,27 @@ def rate_request( pallet_service_details=freightcom_rest_req.PalletServiceDetailsType() if packaging_type == "pallet" else None, ) ), - reference_codes=[payload.reference] if any(payload.reference or "") else [] - ), - customs_data=( - freightcom_rest_req.CustomsDataType( - products=[ - freightcom_rest_req.ProductType( - product_name=item.description, - weight=freightcom_rest_req.WeightType( - unit="kg" if item.weight_unit.upper() == "KG" else "lb", - value=lib.to_decimal(item.weight) - ), - hs_code=item.hs_code, - country_of_origin=item.origin_country, - num_units=item.quantity, - unit_price=freightcom_rest_req.TotalCostType( - currency=item.value_currency, - value=str(int(item.value_amount * 100)) - ), - description=item.description, - fda_regulated="no" - ) for item in customs.commodities - ] if customs and customs.commodities else [], - request_guaranteed_customs_charges=options.request_guaranteed_customs_charges.state if hasattr(options, 'request_guaranteed_customs_charges') else None - ) - if is_ca_to_us and is_package_or_courierpak and customs and customs.commodities - else None + reference_codes=[payload.reference] if any(payload.reference or "") else [], + customs_data=( + freightcom_rest_req.CustomsDataType( + products=[ + freightcom_rest_req.ProductType( + hs_code=item.hs_code, + country_of_origin=item.origin_country, + num_units=item.quantity, + unit_price=freightcom_rest_req.TotalCostType( + currency=item.value_currency, + value=str(int(item.value_amount * 100)) + ), + description=item.description, + fda_regulated="no" + ) for item in customs.commodities + ] if customs and customs.commodities else [], + request_guaranteed_customs_charges=options.request_guaranteed_customs_charges.state if hasattr(options, 'request_guaranteed_customs_charges') else None + ) + if is_ca_to_us and is_package_or_courierpak and customs and customs.commodities + else None + ), ), ) return lib.Serializable(request, lib.to_dict) From 4466d629e2e2eb50dde1f2a02f104ee7e5d8d649 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Mon, 24 Nov 2025 17:07:22 -0500 Subject: [PATCH 12/23] fix: remove redundant packaging type check in customs processing - Simplify conditions for CA to US shipments by removing unnecessary `is_package_or_courierpak` validation. - Ensure customs commodity checks are streamlined for both rate and shipment requests. --- .../freightcom_rest/karrio/providers/freightcom_rest/rate.py | 5 ++--- .../karrio/providers/freightcom_rest/shipment/create.py | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py index 442cf12..0a48459 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py @@ -129,10 +129,9 @@ def rate_request( shipper=payload.shipper, recipient=payload.recipient, weight_unit=packages.weight_unit, - ) if payload.customs else None + ) packaging_type = provider_units.PackagingType.map(packages.package_type or "small_box").value - is_package_or_courierpak = packaging_type in ["package", "courier-pak"] ship_datetime = lib.to_next_business_datetime( options.shipping_date.state or datetime.datetime.now(), current_format="%Y-%m-%dT%H:%M", @@ -275,7 +274,7 @@ def rate_request( ] if customs and customs.commodities else [], request_guaranteed_customs_charges=options.request_guaranteed_customs_charges.state if hasattr(options, 'request_guaranteed_customs_charges') else None ) - if is_ca_to_us and is_package_or_courierpak and customs and customs.commodities + if is_ca_to_us and customs and any(customs.commodities) else None ), ), diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index 8614d33..4cac879 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -173,7 +173,6 @@ def shipment_request( packaging_type = provider_units.PackagingType.map( packages.package_type or "small_box" ).value - is_package_or_courierpak = packaging_type in ["package", "courier-pak"] ship_datetime = lib.to_next_business_datetime( options.shipping_date.state or datetime.datetime.now(), @@ -347,7 +346,7 @@ def shipment_request( ] if customs and customs.commodities else [], request_guaranteed_customs_charges=options.request_guaranteed_customs_charges.state if hasattr(options, 'request_guaranteed_customs_charges') else None ) - if is_ca_to_us and is_package_or_courierpak and customs and customs.commodities + if is_ca_to_us and customs and any(customs.commodities) else None ), ), From cc94416fba342f3d658c6814ee4de53f5f507ca5 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Mon, 24 Nov 2025 19:18:45 -0500 Subject: [PATCH 13/23] feat: add support for DDP shipments and customs payment method handling - Introduce logic for handling Delivered Duty Paid (DDP) shipments, including determining appropriate payment methods. - Add support for selecting customs and duties payment method, with built-in caching for efficiency. - Ensure accurate mapping of tax recipients and guarantee customs charges for DDP shipments. --- .../karrio/providers/freightcom_rest/rate.py | 24 +++++---- .../freightcom_rest/shipment/create.py | 23 +++++++-- .../karrio/providers/freightcom_rest/utils.py | 49 +++++++++++++++++++ 3 files changed, 84 insertions(+), 12 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py index 0a48459..fa269d2 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py @@ -124,12 +124,18 @@ def rate_request( # Create the carrier-specific request object is_intl = shipper.country_code != recipient.country_code is_ca_to_us = shipper.country_code == "CA" and recipient.country_code == "US" + customs = lib.to_customs_info( payload.customs, shipper=payload.shipper, recipient=payload.recipient, weight_unit=packages.weight_unit, ) + commodities = lib.identity( + (customs.commodities if any(customs.commodities) else packages.items) + if any(packages.items) or any(customs.commodities) + else [] + ) packaging_type = provider_units.PackagingType.map(packages.package_type or "small_box").value ship_datetime = lib.to_next_business_datetime( @@ -262,19 +268,19 @@ def rate_request( products=[ freightcom_rest_req.ProductType( hs_code=item.hs_code, - country_of_origin=item.origin_country, - num_units=item.quantity, + country_of_origin=item.origin_country or shipper.country_code, + num_units=item.quantity or 1, unit_price=freightcom_rest_req.TotalCostType( - currency=item.value_currency, - value=str(int(item.value_amount * 100)) + currency=item.value_currency or "CAD", + value=str(int((item.value_amount or 0) * 100)) ), - description=item.description, + description=item.description or item.title, fda_regulated="no" - ) for item in customs.commodities - ] if customs and customs.commodities else [], - request_guaranteed_customs_charges=options.request_guaranteed_customs_charges.state if hasattr(options, 'request_guaranteed_customs_charges') else None + ) for item in (list(commodities) if is_ca_to_us else []) + ], + request_guaranteed_customs_charges=settings.connection_config.request_guaranteed_customs_charges.state or True ) - if is_ca_to_us and customs and any(customs.commodities) + if is_ca_to_us and len(commodities) > 0 else None ), ), diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index 4cac879..ad9b482 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -213,9 +213,23 @@ def shipment_request( if not payment_method_id: raise Exception("No payment method found need to be set in config") + # Check if it's DDP (Delivered Duty Paid) + is_ddp = ( + customs and ( + customs.incoterm == "DDP" or + (customs.duty and customs.duty.paid_by == "sender") + ) + ) if customs else False + + # For DDP shipments with Net Terms, need credit card for customs/duties payment + customs_and_duties_payment_method_id = None + if is_ddp and settings.connection_config.payment_method_type.state == provider_utils.PaymentMethodType.net_terms.value: + customs_and_duties_payment_method_id = settings.customs_and_duties_payment_method + request = freightcom_rest_req.ShipmentRequestType( unique_id=str(uuid.uuid4()), payment_method_id=payment_method_id, + customs_and_duties_payment_method_id=customs_and_duties_payment_method_id, service_id=provider_units.ShippingService.map(payload.service).value_or_key, details=freightcom_rest_req.ShipmentRequestDetailsType( origin=freightcom_rest_req.DestinationType( @@ -377,11 +391,14 @@ def shipment_request( ) for item in customs.commodities ], tax_recipient=freightcom_rest_req.TaxRecipientType( - # they require it to be receiver, has to be enabled in the api - type=provider_units.PaymentType.map( + # For DDP shipments, tax recipient must be 'shipper' + type="shipper" if is_ddp else ( + provider_units.PaymentType.map( customs.duty.paid_by ).value - or "receiver", + if customs.duty and customs.duty.paid_by + else "receiver" + ), name=customs.duty_billing_address.company_name or customs.duty.person_name, address=freightcom_rest_req.AddressType( address_line_1=customs.duty_billing_address.address_line1, diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py index a42909c..bd4f540 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py @@ -58,6 +58,24 @@ def payment_method(self): return new_auth.get("id") + @property + def customs_and_duties_payment_method(self): + + if not self.connection_config.customs_and_duties_payment_method.state: + raise Exception(f"Customs and duties payment method type not set") + cache_key = f"payment|{self.carrier_name}|{self.connection_config.customs_and_duties_payment_method.state}|{self.api_key}" + + payment = self.connection_cache.get(cache_key) or {} + payment_id = payment.get("id") + + if payment_id: + return payment_id + + self.connection_cache.set(cache_key, lambda: get_customs_payment_id(self)) + new_auth = self.connection_cache.get(cache_key) + + return new_auth.get("id") + def download_document_to_base64(file_url: str) -> str: return lib.request( @@ -95,13 +113,44 @@ def get_payment_id(settings: Settings) -> dict: except Exception as e: raise +def get_customs_payment_id(settings: Settings) -> dict: + + try: + from karrio.mappers.freightcom_rest.proxy import Proxy + + proxy = Proxy(settings) + response = proxy._get_payments_methods() + methods = response.deserialize() + + selected_method = next(( + method for method in methods + if settings.connection_config.customs_and_duties_payment_method.type.map( + method.get('type')).name == settings.connection_config.customs_and_duties_payment_method.state + ), None) + + + if not selected_method: + raise Exception(f"Customs payment method {settings.connection_config.customs_and_duties_payment_method.state} not found in API") + + return selected_method + + except Exception as e: + raise + class PaymentMethodType(lib.StrEnum): net_terms = "net-terms" credit_card = "credit-card" +class CustomsPaymentMethodType(lib.StrEnum): + """Payment method type for customs and duties (credit-card only)""" + credit_card = "credit-card" + class ConnectionConfig(lib.Enum): """Carrier specific connection configs""" payment_method_type = lib.OptionEnum("payment_method_type", PaymentMethodType) + customs_and_duties_payment_method = lib.OptionEnum("customs_and_duties_payment_method", CustomsPaymentMethodType) + request_guaranteed_customs_charges = lib.OptionEnum("request_guaranteed_customs_charges", bool, default=True) shipping_options = lib.OptionEnum("shipping_options", list) shipping_services = lib.OptionEnum("shipping_services", list) + From 655d2fefd9f68c0a87a949d140249690c58ab438 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Wed, 24 Dec 2025 14:42:55 -0500 Subject: [PATCH 14/23] feat: add error level mapping support for upstream sync - Add level parameter to error messages (defaults to 'error') - Add _get_level() helper function - Freightcom API v2 does not provide level field, so defaults to 'error' - Required for upstream sync with karrio-community (commit 1a1b158) --- .../karrio/providers/freightcom_rest/error.py | 10 ++++++++++ .../freightcom_rest/tests/freightcom_rest/test_rate.py | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py index e37c3d0..5d9543a 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/error.py @@ -26,6 +26,7 @@ def parse_error_response( if (error.get("details", {}) or error.get("data", {})) else error.get("message") ), + level=_get_level(error), details={ **kwargs, **(error.get('data', {})) @@ -33,3 +34,12 @@ def parse_error_response( ) for error in errors ] + + +def _get_level(error: dict, default_level: str = "error") -> str: + """Map Freightcom error response to standardized level. + + Freightcom API v2 does not provide a level field in error responses. + All error responses default to "error" level. + """ + return default_level diff --git a/plugins/freightcom_rest/tests/freightcom_rest/test_rate.py b/plugins/freightcom_rest/tests/freightcom_rest/test_rate.py index aad8935..6973615 100644 --- a/plugins/freightcom_rest/tests/freightcom_rest/test_rate.py +++ b/plugins/freightcom_rest/tests/freightcom_rest/test_rate.py @@ -449,7 +449,8 @@ def test_parse_error_response(self): { "carrier_id": "freightcom_rest", "carrier_name": "freightcom_rest", - "message": "Unable to get rates", + "message": "Unable to get rates: services: invalid-syntax", + "level": "error", "details": { "services": "invalid-syntax" } From 6c9736f73cc5c8eca437588547207c63379c3560 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Mon, 29 Dec 2025 15:56:04 -0500 Subject: [PATCH 15/23] feat: add is_rate_guaranteed flag to rate meta Add guaranteed rate flag to rate metadata in Freightcom API v2 integration. Extracts is_rate_guaranteed from customs_charge_data and stores it in rate meta dict. --- .../freightcom_rest/karrio/providers/freightcom_rest/rate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py index fa269d2..d31abcc 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py @@ -89,7 +89,7 @@ def _extract_details( meta=dict( service_name=service_name, rate_provider=rate_provider, - request_guaranteed_customs_charges=( + is_rate_guaranteed=( rate.customs_charge_data.is_rate_guaranteed if hasattr(rate, 'customs_charge_data') and rate.customs_charge_data and hasattr(rate.customs_charge_data, 'is_rate_guaranteed') else None From 9411a4048586c580089e413e930c905e8bfd0c1f Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Mon, 29 Dec 2025 17:28:47 -0500 Subject: [PATCH 16/23] feat: implement USMCA customs support for Freightcom API v2 - Add USMCA detection for US, CA, MX routes (bidirectional) - Add use_usmca option to control USMCA handling (defaults to true) - Set cusma_included=True for USMCA-eligible shipments - Add usmca_number option for certification number - Implement PDF document upload via paperless_customs_documents - Fix customs_and_duties_payment_method_id for all DDP shipments - Support non_auto_parts option for products --- .../freightcom_rest/shipment/create.py | 62 ++++++++++++++----- .../karrio/providers/freightcom_rest/units.py | 3 + 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index ad9b482..b8ec016 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -24,6 +24,15 @@ import uuid +def is_usmca_eligible(shipper_country: str, recipient_country: str) -> bool: + """Check if shipment is eligible for USMCA customs handling (US, CA, MX).""" + USMCA_COUNTRIES = {"US", "CA", "MX"} + return ( + (shipper_country in USMCA_COUNTRIES and recipient_country in USMCA_COUNTRIES) and + shipper_country != recipient_country + ) + + def parse_shipment_response( _response: lib.Deserializable[dict], settings: provider_utils.Settings, @@ -180,7 +189,9 @@ def shipment_request( ) is_intl = shipper.country_code != recipient.country_code - is_ca_to_us = shipper.country_code == "CA" and recipient.country_code == "US" + is_usmca_route = is_usmca_eligible(shipper.country_code, recipient.country_code) + use_usmca_option = options.freightcom_use_usmca.state if hasattr(options, 'freightcom_use_usmca') and options.freightcom_use_usmca.state is not None else True + is_usmca = is_usmca_route and use_usmca_option customs = lib.to_customs_info( payload.customs, shipper=payload.shipper, @@ -208,12 +219,6 @@ def shipment_request( ), ) - payment_method_id = settings.payment_method - - if not payment_method_id: - raise Exception("No payment method found need to be set in config") - - # Check if it's DDP (Delivered Duty Paid) is_ddp = ( customs and ( customs.incoterm == "DDP" or @@ -221,9 +226,15 @@ def shipment_request( ) ) if customs else False - # For DDP shipments with Net Terms, need credit card for customs/duties payment + payment_method_id = settings.payment_method + + if not payment_method_id: + raise Exception("No payment method found need to be set in config") + customs_and_duties_payment_method_id = None - if is_ddp and settings.connection_config.payment_method_type.state == provider_utils.PaymentMethodType.net_terms.value: + if settings.connection_config.payment_method_type.state == provider_utils.PaymentMethodType.credit_card.value: + customs_and_duties_payment_method_id = payment_method_id + elif settings.connection_config.customs_and_duties_payment_method.state: customs_and_duties_payment_method_id = settings.customs_and_duties_payment_method request = freightcom_rest_req.ShipmentRequestType( @@ -355,12 +366,14 @@ def shipment_request( value=str(int(item.value_amount * 100)) ), description=item.description, - fda_regulated="no" + fda_regulated="no", + cusma_included=True if is_usmca else None, + non_auto_parts=options.freightcom_non_auto_parts.state if hasattr(options, 'freightcom_non_auto_parts') and options.freightcom_non_auto_parts.state else None, ) for item in customs.commodities ] if customs and customs.commodities else [], - request_guaranteed_customs_charges=options.request_guaranteed_customs_charges.state if hasattr(options, 'request_guaranteed_customs_charges') else None + request_guaranteed_customs_charges=settings.connection_config.request_guaranteed_customs_charges.state ) - if is_ca_to_us and customs and any(customs.commodities) + if is_usmca and customs and any(customs.commodities) else None ), ), @@ -368,7 +381,8 @@ def shipment_request( freightcom_rest_req.CustomsInvoiceType( source="details", broker=freightcom_rest_req.BrokerType( - use_carrier=True, + use_carrier=True, + usmca_number=options.freightcom_usmca_number.state if hasattr(options, 'freightcom_usmca_number') and options.freightcom_usmca_number.state else None, ), details=freightcom_rest_req.CustomsInvoiceDetailsType( products=[ @@ -387,7 +401,9 @@ def shipment_request( value=str(int(item.value_amount * 100)) ), description=item.description, - fda_regulated="no" + fda_regulated="no", + cusma_included=True if is_usmca else None, + non_auto_parts=options.freightcom_non_auto_parts.state if hasattr(options, 'freightcom_non_auto_parts') and options.freightcom_non_auto_parts.state else None, ) for item in customs.commodities ], tax_recipient=freightcom_rest_req.TaxRecipientType( @@ -417,7 +433,23 @@ def shipment_request( ) ) ) - if customs and customs.commodities + if is_intl and customs and customs.commodities + else None + ), + paperless_customs_documents=( + [ + freightcom_rest_req.PaperlessCustomsDocumentType( + type="cusma-form" if doc.get("doc_type") == "cusma-form" else ( + "other" if doc.get("doc_type") == "certificate_of_origin" else "other" + ), + type_other_name=doc.get("doc_type") if doc.get("doc_type") not in ["cusma-form"] else None, + file_name=doc.get("doc_name") or "document.pdf", + file_base64=doc.get("doc_file"), + ) + for doc in (options.doc_files.state or []) + if doc.get("doc_type") in ["cusma-form", "certificate_of_origin"] and doc.get("doc_file") + ] + if hasattr(options, 'doc_files') and options.doc_files.state and is_usmca else None ), #TODO: validate if we need to do pickup in the ship request diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py index 77ccc1f..8162d6c 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py @@ -92,6 +92,9 @@ class ShippingOption(lib.Enum): freightcom_stackable = lib.OptionEnum("stackable", bool) freightcom_payment_method = lib.OptionEnum("payment_method", str) freightcom_request_guaranteed_customs_charges = lib.OptionEnum("request_guaranteed_customs_charges", bool) + freightcom_usmca_number = lib.OptionEnum("usmca_number", str) + freightcom_non_auto_parts = lib.OptionEnum("non_auto_parts", bool) + freightcom_use_usmca = lib.OptionEnum("use_usmca", bool) """ Unified Option type mapping """ # saturday_delivery = freightcom_saturday_pickup_required From e29cccd2ea7e5baa28506e77acd2595c0469006d Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Mon, 29 Dec 2025 18:37:12 -0500 Subject: [PATCH 17/23] feat: add is_customs_rate_guaranteed metadata to shipment response Extract is_rate_guaranteed from shipment.rate.customs_charge_data and include it in shipment meta to indicate if customs charges are guaranteed. --- .../providers/freightcom_rest/shipment/create.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index b8ec016..9389ab5 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -110,6 +110,12 @@ def _extract_details( freightcom_service_id = rate.service_id if hasattr(rate, 'service_id') else "" freightcom_unique_id = shipment.unique_id if hasattr(shipment, 'unique_id') else "" + is_customs_rate_guaranteed = ( + rate.customs_charge_data.is_rate_guaranteed + if hasattr(rate, 'customs_charge_data') and rate.customs_charge_data and hasattr(rate.customs_charge_data, 'is_rate_guaranteed') + else None + ) + else: tracking_number = "" shipment_id = "" @@ -125,6 +131,7 @@ def _extract_details( carrier_tracking_link = "" freightcom_service_id = "" freightcom_unique_id = "" + is_customs_rate_guaranteed = None documents = models.Documents( label=label_base64, @@ -149,7 +156,8 @@ def _extract_details( service_name=service_name, freightcom_service_id=freightcom_service_id, freightcom_unique_id=freightcom_unique_id, - freightcom_shipment_identifier=shipment_id + freightcom_shipment_identifier=shipment_id, + is_customs_rate_guaranteed=is_customs_rate_guaranteed, ), ) From 2ea075ce6edb32ff865e235d9bb2ff41b22d0b63 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Tue, 6 Jan 2026 15:08:53 -0500 Subject: [PATCH 18/23] feat: add freightcom_doc_files option with doc_files fallback - Add carrier-specific freightcom_doc_files option to avoid conflicts with FedEx - FedEx requires documentId (pre-uploaded) while Freightcom accepts base64 directly - Prevents SHIPMENTS.DOCUMENTID.INVALID errors when rate shopping across carriers - Maintains backward compatibility with doc_files fallback - See GitLab issue #11 for migration details --- .../providers/freightcom_rest/shipment/create.py | 13 +++++++++++-- .../karrio/providers/freightcom_rest/units.py | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index 9389ab5..f1260a7 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -454,10 +454,19 @@ def shipment_request( file_name=doc.get("doc_name") or "document.pdf", file_base64=doc.get("doc_file"), ) - for doc in (options.doc_files.state or []) + for doc in ( + (options.freightcom_doc_files.state or []) + if hasattr(options, 'freightcom_doc_files') and options.freightcom_doc_files.state + else (options.doc_files.state or []) + if hasattr(options, 'doc_files') and options.doc_files.state + else [] + ) if doc.get("doc_type") in ["cusma-form", "certificate_of_origin"] and doc.get("doc_file") ] - if hasattr(options, 'doc_files') and options.doc_files.state and is_usmca + if is_usmca and ( + (hasattr(options, 'freightcom_doc_files') and options.freightcom_doc_files.state) + or (hasattr(options, 'doc_files') and options.doc_files.state) + ) else None ), #TODO: validate if we need to do pickup in the ship request diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py index 8162d6c..05ea3e5 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py @@ -95,6 +95,7 @@ class ShippingOption(lib.Enum): freightcom_usmca_number = lib.OptionEnum("usmca_number", str) freightcom_non_auto_parts = lib.OptionEnum("non_auto_parts", bool) freightcom_use_usmca = lib.OptionEnum("use_usmca", bool) + freightcom_doc_files = lib.OptionEnum("freightcom_doc_files", lib.to_dict) """ Unified Option type mapping """ # saturday_delivery = freightcom_saturday_pickup_required From f75bd38bee72604ef859c88d2ff5f08921bfc3ba Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Mon, 12 Jan 2026 16:10:13 -0500 Subject: [PATCH 19/23] refactor: simplify customs handling to match other carriers - Simplified commodities selection logic - Removed complex nested conditionals - Aligned product fields (product_name=item.title, description=item.description) - Added weight field to products - Simplified customs check (has_customs) - Updated both rate and shipment to use consistent pattern - Simplified filter condition to only check item.hs_code - Added is_usmca_eligible helper function for customs handling --- .../karrio/providers/freightcom_rest/rate.py | 48 +++++++--- .../freightcom_rest/shipment/create.py | 95 +++++++++---------- .../karrio/providers/freightcom_rest/utils.py | 9 ++ 3 files changed, 88 insertions(+), 64 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py index d31abcc..b6b8e2b 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py @@ -123,7 +123,9 @@ def rate_request( # Create the carrier-specific request object is_intl = shipper.country_code != recipient.country_code - is_ca_to_us = shipper.country_code == "CA" and recipient.country_code == "US" + is_usmca_route = provider_utils.is_usmca_eligible(shipper.country_code, recipient.country_code) + use_usmca_option = options.freightcom_use_usmca.state if hasattr(options, 'freightcom_use_usmca') and options.freightcom_use_usmca.state is not None else True + is_usmca = is_usmca_route and use_usmca_option customs = lib.to_customs_info( payload.customs, @@ -131,10 +133,16 @@ def rate_request( recipient=payload.recipient, weight_unit=packages.weight_unit, ) - commodities = lib.identity( - (customs.commodities if any(customs.commodities) else packages.items) - if any(packages.items) or any(customs.commodities) - else [] + + # Use customs.commodities if available, otherwise fall back to packages.items + commodities = customs.commodities if customs and customs.is_defined and any(customs.commodities) else packages.items + + # Only include customs for international shipments with valid customs data + has_customs = ( + is_intl + and customs + and customs.is_defined + and any(commodities) ) packaging_type = provider_units.PackagingType.map(packages.package_type or "small_box").value @@ -267,23 +275,37 @@ def rate_request( freightcom_rest_req.CustomsDataType( products=[ freightcom_rest_req.ProductType( + product_name=item.title, + weight=freightcom_rest_req.WeightType( + unit="kg" if item.weight_unit.upper() == "KG" else "lb", + value=lib.to_decimal(item.weight) + ), hs_code=item.hs_code, - country_of_origin=item.origin_country or shipper.country_code, - num_units=item.quantity or 1, + country_of_origin=item.origin_country, + num_units=item.quantity, unit_price=freightcom_rest_req.TotalCostType( - currency=item.value_currency or "CAD", - value=str(int((item.value_amount or 0) * 100)) + currency=item.value_currency, + value=str(int(item.value_amount * 100)) + ), + description=item.description, + fda_regulated="no", + cusma_included=True if is_usmca else None, + non_auto_parts=( + options.freightcom_non_auto_parts.state + if hasattr(options, 'freightcom_non_auto_parts') and options.freightcom_non_auto_parts.state + else None ), - description=item.description or item.title, - fda_regulated="no" - ) for item in (list(commodities) if is_ca_to_us else []) + ) + for item in commodities + if item.hs_code ], request_guaranteed_customs_charges=settings.connection_config.request_guaranteed_customs_charges.state or True ) - if is_ca_to_us and len(commodities) > 0 + if has_customs else None ), ), ) + return lib.Serializable(request, lib.to_dict) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index f1260a7..c8b8805 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -24,15 +24,6 @@ import uuid -def is_usmca_eligible(shipper_country: str, recipient_country: str) -> bool: - """Check if shipment is eligible for USMCA customs handling (US, CA, MX).""" - USMCA_COUNTRIES = {"US", "CA", "MX"} - return ( - (shipper_country in USMCA_COUNTRIES and recipient_country in USMCA_COUNTRIES) and - shipper_country != recipient_country - ) - - def parse_shipment_response( _response: lib.Deserializable[dict], settings: provider_utils.Settings, @@ -197,7 +188,7 @@ def shipment_request( ) is_intl = shipper.country_code != recipient.country_code - is_usmca_route = is_usmca_eligible(shipper.country_code, recipient.country_code) + is_usmca_route = provider_utils.is_usmca_eligible(shipper.country_code, recipient.country_code) use_usmca_option = options.freightcom_use_usmca.state if hasattr(options, 'freightcom_use_usmca') and options.freightcom_use_usmca.state is not None else True is_usmca = is_usmca_route and use_usmca_option customs = lib.to_customs_info( @@ -205,34 +196,23 @@ def shipment_request( shipper=payload.shipper, recipient=payload.recipient, weight_unit=packages.weight_unit, - default_to=( - models.Customs( - commodities=( - packages.items - if any(packages.items) - else [ - models.Commodity( - quantity=1, - sku=f"000{index}", - weight=pkg.weight.value, - weight_unit=pkg.weight_unit.value, - description=pkg.parcel.content, - ) - for index, pkg in enumerate(packages, start=1) - ] - ) - ) - if is_intl - else None - ), + ) + + # Use customs.commodities if available, otherwise fall back to packages.items + commodities = customs.commodities if customs and customs.is_defined and any(customs.commodities) else packages.items + + # Only include customs for international shipments with valid customs data + has_customs = ( + is_intl + and customs + and customs.is_defined + and any(commodities) ) is_ddp = ( - customs and ( - customs.incoterm == "DDP" or - (customs.duty and customs.duty.paid_by == "sender") - ) - ) if customs else False + customs.incoterm == "DDP" + or (customs.duty and customs.duty.paid_by == "sender") + ) if customs and customs.is_defined else False payment_method_id = settings.payment_method @@ -361,7 +341,7 @@ def shipment_request( freightcom_rest_req.CustomsDataType( products=[ freightcom_rest_req.ProductType( - product_name=item.description, + product_name=item.title, weight=freightcom_rest_req.WeightType( unit="kg" if item.weight_unit.upper() == "KG" else "lb", value=lib.to_decimal(item.weight) @@ -376,12 +356,18 @@ def shipment_request( description=item.description, fda_regulated="no", cusma_included=True if is_usmca else None, - non_auto_parts=options.freightcom_non_auto_parts.state if hasattr(options, 'freightcom_non_auto_parts') and options.freightcom_non_auto_parts.state else None, - ) for item in customs.commodities - ] if customs and customs.commodities else [], + non_auto_parts=( + options.freightcom_non_auto_parts.state + if hasattr(options, 'freightcom_non_auto_parts') and options.freightcom_non_auto_parts.state + else None + ), + ) + for item in commodities + if item.hs_code + ], request_guaranteed_customs_charges=settings.connection_config.request_guaranteed_customs_charges.state ) - if is_usmca and customs and any(customs.commodities) + if has_customs else None ), ), @@ -390,12 +376,16 @@ def shipment_request( source="details", broker=freightcom_rest_req.BrokerType( use_carrier=True, - usmca_number=options.freightcom_usmca_number.state if hasattr(options, 'freightcom_usmca_number') and options.freightcom_usmca_number.state else None, + usmca_number=( + options.freightcom_usmca_number.state + if hasattr(options, 'freightcom_usmca_number') and options.freightcom_usmca_number.state + else None + ), ), details=freightcom_rest_req.CustomsInvoiceDetailsType( products=[ freightcom_rest_req.ProductType( - product_name=item.description, + product_name=item.title, weight=freightcom_rest_req.WeightType( unit="kg" if item.weight_unit.upper() == "KG" else "lb", value=lib.to_decimal(item.weight) @@ -405,21 +395,24 @@ def shipment_request( num_units=item.quantity, unit_price=freightcom_rest_req.TotalCostType( currency=item.value_currency, - # the api expect to be a whole number like 16900 for 169.00 value=str(int(item.value_amount * 100)) ), description=item.description, fda_regulated="no", cusma_included=True if is_usmca else None, - non_auto_parts=options.freightcom_non_auto_parts.state if hasattr(options, 'freightcom_non_auto_parts') and options.freightcom_non_auto_parts.state else None, - ) for item in customs.commodities + non_auto_parts=( + options.freightcom_non_auto_parts.state + if hasattr(options, 'freightcom_non_auto_parts') and options.freightcom_non_auto_parts.state + else None + ), + ) + for item in commodities + if item.hs_code ], tax_recipient=freightcom_rest_req.TaxRecipientType( - # For DDP shipments, tax recipient must be 'shipper' - type="shipper" if is_ddp else ( - provider_units.PaymentType.map( - customs.duty.paid_by - ).value + type=( + "shipper" if is_ddp + else provider_units.PaymentType.map(customs.duty.paid_by).value if customs.duty and customs.duty.paid_by else "receiver" ), @@ -441,7 +434,7 @@ def shipment_request( ) ) ) - if is_intl and customs and customs.commodities + if has_customs else None ), paperless_customs_documents=( diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py index bd4f540..0ae482c 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py @@ -89,6 +89,15 @@ def ceil(value: typing.Optional[float]) -> typing.Optional[int]: return None return math.ceil(value) + +def is_usmca_eligible(shipper_country: str, recipient_country: str) -> bool: + """Check if shipment is eligible for USMCA customs handling (US, CA, MX).""" + USMCA_COUNTRIES = {"US", "CA", "MX"} + return ( + (shipper_country in USMCA_COUNTRIES and recipient_country in USMCA_COUNTRIES) and + shipper_country != recipient_country + ) + def get_payment_id(settings: Settings) -> dict: try: From 1fd57e151bffaadb6689894b1bb10b6f0b11e2d1 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Thu, 15 Jan 2026 14:56:57 -0500 Subject: [PATCH 20/23] fix(freightcom_rest): use imperial units (LB/IN) instead of metric (KG/CM) Freightcom REST expects imperial units and converts/rounds them. By sending metric units, Karrio was converting to metric, then Freightcom was converting back to imperial and rounding, causing double conversion and rounding errors. Changed all weight and dimension units from metric (KG/CM) to imperial (LB/IN) in both rate and shipment requests to match Freightcom REST's expectations. --- .../karrio/providers/freightcom_rest/rate.py | 28 +++++++++---------- .../freightcom_rest/shipment/create.py | 28 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py index b6b8e2b..42e1512 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py @@ -218,14 +218,14 @@ def rate_request( freightcom_rest_req.PalletType( measurements=freightcom_rest_req.PackageMeasurementsType( weight=freightcom_rest_req.WeightType( - unit="kg", - value=parcel.weight.KG + unit="lb", + value=parcel.weight.LB ), cuboid=freightcom_rest_req.CuboidType( - unit="cm", - l=parcel.length.CM, - w=parcel.width.CM, - h=parcel.height.CM + unit="in", + l=parcel.length.IN, + w=parcel.width.IN, + h=parcel.height.IN ) ), description=parcel.description, @@ -236,14 +236,14 @@ def rate_request( freightcom_rest_req.PackageType( measurements=freightcom_rest_req.PackageMeasurementsType( weight=freightcom_rest_req.WeightType( - unit="kg", - value=parcel.weight.KG + unit="lb", + value=parcel.weight.LB ), cuboid=freightcom_rest_req.CuboidType( - unit="cm", - l=parcel.length.CM, - w=parcel.width.CM, - h=parcel.height.CM + unit="in", + l=parcel.length.IN, + w=parcel.width.IN, + h=parcel.height.IN ) ), description=parcel.description, @@ -253,8 +253,8 @@ def rate_request( freightcom_rest_req.CourierpakType( measurements=freightcom_rest_req.CourierpakMeasurementsType( weight=freightcom_rest_req.WeightType( - unit="kg", - value=parcel.weight.KG + unit="lb", + value=parcel.weight.LB ), ), description=parcel.description, diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index c8b8805..b9333e8 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -293,14 +293,14 @@ def shipment_request( freightcom_rest_req.PalletType( measurements=freightcom_rest_req.PackageMeasurementsType( weight=freightcom_rest_req.WeightType( - unit="kg", - value=parcel.weight.KG + unit="lb", + value=parcel.weight.LB ), cuboid=freightcom_rest_req.CuboidType( - unit="cm", - l=parcel.length.CM, - w=parcel.width.CM, - h=parcel.height.CM + unit="in", + l=parcel.length.IN, + w=parcel.width.IN, + h=parcel.height.IN ) ), description=parcel.description or "N/A", @@ -311,14 +311,14 @@ def shipment_request( freightcom_rest_req.PackageType( measurements=freightcom_rest_req.PackageMeasurementsType( weight=freightcom_rest_req.WeightType( - unit="kg", - value=parcel.weight.KG + unit="lb", + value=parcel.weight.LB ), cuboid=freightcom_rest_req.CuboidType( - unit="cm", - l=parcel.length.CM, - w=parcel.width.CM, - h=parcel.height.CM + unit="in", + l=parcel.length.IN, + w=parcel.width.IN, + h=parcel.height.IN ) ), description=parcel.description or "N/A", @@ -328,8 +328,8 @@ def shipment_request( freightcom_rest_req.CourierpakType( measurements=freightcom_rest_req.CourierpakMeasurementsType( weight=freightcom_rest_req.WeightType( - unit="kg", - value=parcel.weight.KG + unit="lb", + value=parcel.weight.LB ), ), description=parcel.description or "N/A", From 288d2432cfe6632ac898783cfb7b6369e8183b12 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Thu, 15 Jan 2026 15:05:37 -0500 Subject: [PATCH 21/23] fix(freightcom_rest): correct freightcom_use_usmca option check logic Changed from checking hasattr() and state is not None to using 'in' operator to properly detect if the option was actually provided in the options dict. Also changed default from True to False when option is not provided. The hasattr() check was always True due to __getattr__, so it didn't properly detect when the option was missing. Using 'in options' correctly checks if the option exists in the internal options dict. --- .../freightcom_rest/karrio/providers/freightcom_rest/rate.py | 2 +- .../karrio/providers/freightcom_rest/shipment/create.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py index 42e1512..9947eef 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py @@ -124,7 +124,7 @@ def rate_request( # Create the carrier-specific request object is_intl = shipper.country_code != recipient.country_code is_usmca_route = provider_utils.is_usmca_eligible(shipper.country_code, recipient.country_code) - use_usmca_option = options.freightcom_use_usmca.state if hasattr(options, 'freightcom_use_usmca') and options.freightcom_use_usmca.state is not None else True + use_usmca_option = options.freightcom_use_usmca.state if "freightcom_use_usmca" in options else False is_usmca = is_usmca_route and use_usmca_option customs = lib.to_customs_info( diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index b9333e8..9f28290 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -189,7 +189,7 @@ def shipment_request( is_intl = shipper.country_code != recipient.country_code is_usmca_route = provider_utils.is_usmca_eligible(shipper.country_code, recipient.country_code) - use_usmca_option = options.freightcom_use_usmca.state if hasattr(options, 'freightcom_use_usmca') and options.freightcom_use_usmca.state is not None else True + use_usmca_option = options.freightcom_use_usmca.state if "freightcom_use_usmca" in options else False is_usmca = is_usmca_route and use_usmca_option customs = lib.to_customs_info( payload.customs, From af169bb82b36763cee6360d6fa86fa0959e69c12 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Thu, 15 Jan 2026 15:31:04 -0500 Subject: [PATCH 22/23] fix(freightcom_rest): remove product_name and weight from rate request ProductType The Freightcom API v2 rate request schema does not include product_name or weight fields in the ProductType (only shipment request has these fields). This bug was introduced in commit f75bd38 when these fields were incorrectly added to the rate request. Also fixed freightcom_non_auto_parts option check to use 'in' operator instead of hasattr() for consistency with other option checks. Fixes error when creating rate requests with customs data. --- .../karrio/providers/freightcom_rest/rate.py | 7 +------ .../karrio/providers/freightcom_rest/shipment/create.py | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py index 9947eef..65ef2df 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py @@ -275,11 +275,6 @@ def rate_request( freightcom_rest_req.CustomsDataType( products=[ freightcom_rest_req.ProductType( - product_name=item.title, - weight=freightcom_rest_req.WeightType( - unit="kg" if item.weight_unit.upper() == "KG" else "lb", - value=lib.to_decimal(item.weight) - ), hs_code=item.hs_code, country_of_origin=item.origin_country, num_units=item.quantity, @@ -292,7 +287,7 @@ def rate_request( cusma_included=True if is_usmca else None, non_auto_parts=( options.freightcom_non_auto_parts.state - if hasattr(options, 'freightcom_non_auto_parts') and options.freightcom_non_auto_parts.state + if "freightcom_non_auto_parts" in options and options.freightcom_non_auto_parts.state else None ), ) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index 9f28290..6493a98 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -358,7 +358,7 @@ def shipment_request( cusma_included=True if is_usmca else None, non_auto_parts=( options.freightcom_non_auto_parts.state - if hasattr(options, 'freightcom_non_auto_parts') and options.freightcom_non_auto_parts.state + if "freightcom_non_auto_parts" in options and options.freightcom_non_auto_parts.state else None ), ) @@ -402,7 +402,7 @@ def shipment_request( cusma_included=True if is_usmca else None, non_auto_parts=( options.freightcom_non_auto_parts.state - if hasattr(options, 'freightcom_non_auto_parts') and options.freightcom_non_auto_parts.state + if "freightcom_non_auto_parts" in options and options.freightcom_non_auto_parts.state else None ), ) From 7c59f24e0cebf86ab3c36c5567568441c6f617d4 Mon Sep 17 00:00:00 2001 From: Jacob Shilitz Date: Thu, 22 Jan 2026 14:41:09 -0500 Subject: [PATCH 23/23] fix(freightcom_rest): prevent paperless_customs_documents from being sent as [null] - Only include paperless_customs_documents for USMCA shipments with valid documents - Use conditional kwargs to avoid jstruct converting None to [None] - Only set customs_and_duties_payment_method_id when has_customs is True - Fixes API error: 'paperless_customs_documents: allowed for international shipments only' --- .../freightcom_rest/shipment/create.py | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py index 6493a98..d33cefa 100644 --- a/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py +++ b/plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py @@ -220,10 +220,35 @@ def shipment_request( raise Exception("No payment method found need to be set in config") customs_and_duties_payment_method_id = None - if settings.connection_config.payment_method_type.state == provider_utils.PaymentMethodType.credit_card.value: - customs_and_duties_payment_method_id = payment_method_id - elif settings.connection_config.customs_and_duties_payment_method.state: - customs_and_duties_payment_method_id = settings.customs_and_duties_payment_method + if has_customs: + if settings.connection_config.payment_method_type.state == provider_utils.PaymentMethodType.credit_card.value: + customs_and_duties_payment_method_id = payment_method_id + elif settings.connection_config.customs_and_duties_payment_method.state: + customs_and_duties_payment_method_id = settings.customs_and_duties_payment_method + + # Build paperless customs documents only for USMCA shipments with valid documents + paperless_docs = None + if is_usmca: + doc_files = ( + (options.freightcom_doc_files.state or []) + if hasattr(options, 'freightcom_doc_files') and options.freightcom_doc_files.state + else (options.doc_files.state or []) + if hasattr(options, 'doc_files') and options.doc_files.state + else [] + ) + docs_list = [ + freightcom_rest_req.PaperlessCustomsDocumentType( + type="cusma-form" if doc.get("doc_type") == "cusma-form" else ( + "other" if doc.get("doc_type") == "certificate_of_origin" else "other" + ), + type_other_name=doc.get("doc_type") if doc.get("doc_type") not in ["cusma-form"] else None, + file_name=doc.get("doc_name") or "document.pdf", + file_base64=doc.get("doc_file"), + ) + for doc in doc_files + if doc.get("doc_type") in ["cusma-form", "certificate_of_origin"] and doc.get("doc_file") + ] + paperless_docs = docs_list if docs_list else None request = freightcom_rest_req.ShipmentRequestType( unique_id=str(uuid.uuid4()), @@ -437,35 +462,11 @@ def shipment_request( if has_customs else None ), - paperless_customs_documents=( - [ - freightcom_rest_req.PaperlessCustomsDocumentType( - type="cusma-form" if doc.get("doc_type") == "cusma-form" else ( - "other" if doc.get("doc_type") == "certificate_of_origin" else "other" - ), - type_other_name=doc.get("doc_type") if doc.get("doc_type") not in ["cusma-form"] else None, - file_name=doc.get("doc_name") or "document.pdf", - file_base64=doc.get("doc_file"), - ) - for doc in ( - (options.freightcom_doc_files.state or []) - if hasattr(options, 'freightcom_doc_files') and options.freightcom_doc_files.state - else (options.doc_files.state or []) - if hasattr(options, 'doc_files') and options.doc_files.state - else [] - ) - if doc.get("doc_type") in ["cusma-form", "certificate_of_origin"] and doc.get("doc_file") - ] - if is_usmca and ( - (hasattr(options, 'freightcom_doc_files') and options.freightcom_doc_files.state) - or (hasattr(options, 'doc_files') and options.doc_files.state) - ) - else None - ), #TODO: validate if we need to do pickup in the ship request # pickup_details=freightcom.PickupDetailsType( # # ) + **({"paperless_customs_documents": paperless_docs} if paperless_docs else {}), ) return lib.Serializable(