diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile index d8865cda43..72151c9d1c 100644 --- a/docker/dev.Dockerfile +++ b/docker/dev.Dockerfile @@ -7,5 +7,6 @@ RUN apt-get update -y && apt-get install -y gcc libpango1.0-0 libpangoft2-1.0-0 WORKDIR /karrio ENV PATH="/karrio/.venv/karrio/bin:$PATH" EXPOSE 5002 +EXPOSE 3000 -ENV PORT 5002 \ No newline at end of file +ENV PORT 5002 diff --git a/modules/connectors/freightcomv2/README.md b/modules/connectors/freightcomv2/README.md new file mode 100644 index 0000000000..29d64fb918 --- /dev/null +++ b/modules/connectors/freightcomv2/README.md @@ -0,0 +1,31 @@ + +# karrio.freightcomv2 + +This package is a freightcom v2 extension of the [karrio](https://pypi.org/project/karrio) multi carrier shipping SDK. + +## Requirements + +`Python 3.7+` + +## Installation + +```bash +pip install karrio.freightcomv2 +``` + +## Usage + +```python +import karrio +from karrio.mappers.freightcomv2.settings import Settings + + +# Initialize a carrier gateway +freightcomv2 = karrio.gateway["freightcomv2"].create( + Settings( + ... + ) +) +``` + +Check the [Karrio Mutli-carrier SDK docs](https://docs.karrio.io) for Shipping API requests diff --git a/modules/connectors/freightcomv2/generate b/modules/connectors/freightcomv2/generate new file mode 100755 index 0000000000..99709f3a45 --- /dev/null +++ b/modules/connectors/freightcomv2/generate @@ -0,0 +1,27 @@ +SCHEMAS=./schemas +LIB_MODULES=./karrio/schemas/freightcomv2 +find "${LIB_MODULES}" -name "*.py" -exec rm -r {} \; +touch "${LIB_MODULES}/__init__.py" + +quicktype () { + echo "Generating $1..." + docker run -it -v $PWD:/app -e SCHEMAS=/app/schemas -e LIB_MODULES=/app/karrio/schemas/freightcomv2 \ + karrio/tools /quicktype/script/quicktype --no-uuids --no-date-times --no-enums --src-lang json --lang jstruct \ + --no-nice-property-names --all-properties-optional --type-as-suffix $@ +} + +quicktype --src="${SCHEMAS}/error.json" --out="${LIB_MODULES}/error.py" +quicktype --src="${SCHEMAS}/create_shipment_request.json" --out="${LIB_MODULES}/create_shipment_request.py" +quicktype --src="${SCHEMAS}/create_shipment_response.json" --out="${LIB_MODULES}/create_shipment_response.py" +quicktype --src="${SCHEMAS}/rate_request.json" --out="${LIB_MODULES}/rate_request.py" +quicktype --src="${SCHEMAS}/rate_response.json" --out="${LIB_MODULES}/rate_response.py" +quicktype --src="${SCHEMAS}/tracking_response.json" --out="${LIB_MODULES}/tracking_response.py" + + + +#quicktype --src="${SCHEMAS}/error_response.json" --out="${LIB_MODULES}/error_response.py" +#quicktype --src="${SCHEMAS}/purchase_label_request.json" --out="${LIB_MODULES}/purchase_label_request.py" +#quicktype --src="${SCHEMAS}/purchase_label_response.json" --out="${LIB_MODULES}/purchase_label_response.py" +#quicktype --src="${SCHEMAS}/purchase_shipment_request.json" --out="${LIB_MODULES}/purchase_shipment_request.py" +#quicktype --src="${SCHEMAS}/purchase_shipment_response.json" --out="${LIB_MODULES}/purchase_shipment_response.py" +#quicktype --src="${SCHEMAS}/shipping_label.json" --out="${LIB_MODULES}/shipping_label.py" diff --git a/modules/connectors/freightcomv2/karrio/mappers/freightcomv2/__init__.py b/modules/connectors/freightcomv2/karrio/mappers/freightcomv2/__init__.py new file mode 100644 index 0000000000..136688dba4 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/mappers/freightcomv2/__init__.py @@ -0,0 +1,19 @@ + +from karrio.core.metadata import Metadata + +from karrio.mappers.freightcomv2.mapper import Mapper +from karrio.mappers.freightcomv2.proxy import Proxy +from karrio.mappers.freightcomv2.settings import Settings +import karrio.providers.freightcomv2.units as units + + +METADATA = Metadata( + id="freightcomv2", + label="freightcom v2", + # Integrations + Mapper=Mapper, + Proxy=Proxy, + Settings=Settings, + # Data Units + is_hub=False +) diff --git a/modules/connectors/freightcomv2/karrio/mappers/freightcomv2/mapper.py b/modules/connectors/freightcomv2/karrio/mappers/freightcomv2/mapper.py new file mode 100644 index 0000000000..0152347955 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/mappers/freightcomv2/mapper.py @@ -0,0 +1,55 @@ + +"""Karrio freightcom v2 client mapper.""" + +import typing +import karrio.lib as lib +import karrio.api.mapper as mapper +import karrio.core.models as models +import karrio.providers.freightcomv2 as provider +import karrio.mappers.freightcomv2.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/modules/connectors/freightcomv2/karrio/mappers/freightcomv2/proxy.py b/modules/connectors/freightcomv2/karrio/mappers/freightcomv2/proxy.py new file mode 100644 index 0000000000..d93f7ba675 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/mappers/freightcomv2/proxy.py @@ -0,0 +1,75 @@ + +"""Karrio freightcom v2 client proxy.""" + +import karrio.lib as lib +import karrio.api.proxy as proxy +import karrio.mappers.freightcomv2.settings as provider_settings + + +class Proxy(proxy.Proxy): + settings: provider_settings.Settings + + def get_rates(self, request: lib.Serializable) -> lib.Deserializable[str]: + response = lib.request( + url=f"{self.settings.server_url}/rate", + data=request.serialize(), + trace=self.trace_as("json"), + method="POST", + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + #"X-API-VERSION": "1", + "Authorization": f"{self.settings.apiKey}" + }, + ) + + return lib.Deserializable(response, lib.to_dict) + + def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]: + response = lib.request( + url=f"{self.settings.server_url}/shipment", + data=request.serialize(), + trace=self.trace_as("json"), + method="POST", + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + #"X-API-VERSION": "1", + "Authorization": f"{self.settings.apiKey}" + }, + ) + + return lib.Deserializable(response, lib.to_dict) + + def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]: + response = lib.request( + url=f"{self.settings.server_url}/shipment/{request.serialize()['shipment_id']}", + data=request.serialize(), + trace=self.trace_as("json"), + method="DELETE", + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + #"X-API-VERSION": "1", + "Authorization": f"{self.settings.apiKey}" + }, + ) + + return lib.Deserializable(response, lib.to_dict) + + def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[str]: + payload = request.serialize() + response = lib.request( + url=f"{self.settings.server_url}/shipment/{payload['shipment_id']}/tracking-events", + data=request.serialize(), + trace=self.trace_as("json"), + method="POST", + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + #"X-API-VERSION": "1", + "Authorization": f"{self.settings.apiKey}" + }, + ) + + return lib.Deserializable(response, lib.to_dict) diff --git a/modules/connectors/freightcomv2/karrio/mappers/freightcomv2/settings.py b/modules/connectors/freightcomv2/karrio/mappers/freightcomv2/settings.py new file mode 100644 index 0000000000..96a017dea6 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/mappers/freightcomv2/settings.py @@ -0,0 +1,21 @@ + +"""Karrio freightcom v2 client settings.""" + +import attr +import karrio.providers.freightcomv2.utils as provider_utils + + +@attr.s(auto_attribs=True) +class Settings(provider_utils.Settings): + """freightcom v2 connection settings.""" + + # required carrier specific properties + apiKey: str + + # generic properties + id: str = None + test_mode: bool = False + carrier_id: str = "freightcomv2" + account_country_code: str = None + metadata: dict = {} + config: dict = {} diff --git a/modules/connectors/freightcomv2/karrio/providers/freightcomv2/__init__.py b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/__init__.py new file mode 100644 index 0000000000..f924fcacbf --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/__init__.py @@ -0,0 +1,13 @@ + +from karrio.providers.freightcomv2.utils import Settings +from karrio.providers.freightcomv2.rate import parse_rate_response, rate_request +from karrio.providers.freightcomv2.shipment import ( + parse_shipment_cancel_response, + parse_shipment_response, + shipment_cancel_request, + shipment_request, +) +from karrio.providers.freightcomv2.tracking import ( + parse_tracking_response, + tracking_request, +) diff --git a/modules/connectors/freightcomv2/karrio/providers/freightcomv2/error.py b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/error.py new file mode 100644 index 0000000000..82d67d40d4 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/error.py @@ -0,0 +1,24 @@ + +import typing +import karrio.lib as lib +import karrio.core.models as models +import karrio.providers.freightcomv2.utils as provider_utils + + +def parse_error_response( + response: dict, + settings: provider_utils.Settings, + **kwargs, +) -> typing.List[models.Message]: + errors = [] # compute the carrier error object list + + return [ + models.Message( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + code="", + message="", + details={**kwargs}, + ) + for error in errors + ] diff --git a/modules/connectors/freightcomv2/karrio/providers/freightcomv2/rate.py b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/rate.py new file mode 100644 index 0000000000..77763edac6 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/rate.py @@ -0,0 +1,78 @@ + +import typing +import karrio.lib as lib +import karrio.core.units as units +import karrio.core.models as models +import karrio.providers.freightcomv2.error as error +import karrio.providers.freightcomv2.utils as provider_utils +import karrio.providers.freightcomv2.units as provider_units + + +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) + rates = [_extract_details(rate, settings) for rate in response] + return rates, messages + + +def _extract_details( + data: dict, + settings: provider_utils.Settings, +) -> models.RateDetails: + rate = None # parse carrier rate type + + # Extract necessary details from the rate response + total_charge = data.get("TotalCharges", 0.0) + currency = "CAD" # Assuming currency is CAD, update as needed + service = data.get("Standard", "Regular") # Update with actual service key, if available + transit_days = 0 # Transit days key unknown, update if available in response + + print("total_charge", total_charge) + print("currency", currency) + print("service", service) + print("transit_days", transit_days) + + rate_details = models.RateDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + service=service, + total_charge=total_charge, + currency=currency, + transit_days=transit_days, + + ) + print(rate_details) + return rate_details + + + + return models.RateDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + service="", # extract service from rate + total_charge=0.0, # extract the rate total rate cost + currency="", # extract the rate pricing currency + transit_days=0, # extract the rate transit days + meta=dict( + service_name="", # extract the rate service human readable name + ), + ) + + +def rate_request( + payload: models.RateRequest, + settings: provider_utils.Settings, +) -> lib.Serializable: + packages = lib.to_packages(payload.parcels) # preprocess the request parcels + services = lib.to_services(payload.services, provider_units.ShippingService) # preprocess the request services + options = lib.to_shipping_options( + payload.options, + package_options=packages.options, + ) # preprocess the request options + + request = None # map data to convert karrio model to freightcomv2 specific type + + return lib.Serializable(request) diff --git a/modules/connectors/freightcomv2/karrio/providers/freightcomv2/shipment/__init__.py b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/shipment/__init__.py new file mode 100644 index 0000000000..6bcd6b0bd9 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/shipment/__init__.py @@ -0,0 +1,9 @@ + +from karrio.providers.freightcomv2.shipment.create import ( + parse_shipment_response, + shipment_request, +) +from karrio.providers.freightcomv2.shipment.cancel import ( + parse_shipment_cancel_response, + shipment_cancel_request, +) diff --git a/modules/connectors/freightcomv2/karrio/providers/freightcomv2/shipment/cancel.py b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/shipment/cancel.py new file mode 100644 index 0000000000..1e0859b68e --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/shipment/cancel.py @@ -0,0 +1,37 @@ + +import typing +import karrio.lib as lib +import karrio.core.models as models +import karrio.providers.freightcomv2.error as error +import karrio.providers.freightcomv2.utils as provider_utils +import karrio.providers.freightcomv2.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]]: + response = _response.deserialize() + messages = error.parse_error_response(response, settings) + success = True # compute shipment cancel success state + + 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 shipment_cancel_request( + payload: models.ShipmentCancelRequest, + settings: provider_utils.Settings, +) -> lib.Serializable: + + request = None # map data to convert karrio model to freightcomv2 specific type + + return lib.Serializable(request) diff --git a/modules/connectors/freightcomv2/karrio/providers/freightcomv2/shipment/create.py b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/shipment/create.py new file mode 100644 index 0000000000..6cdf6a8925 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/shipment/create.py @@ -0,0 +1,64 @@ + +import typing +import karrio.lib as lib +import karrio.core.units as units +import karrio.core.models as models +import karrio.providers.freightcomv2.error as error +import karrio.providers.freightcomv2.utils as provider_utils +import karrio.providers.freightcomv2.units as provider_units + + +def parse_shipment_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) + shipment = ( + _extract_details(response, settings) + if "tracking_number" in response + else None + ) + + return shipment, messages + + +def _extract_details( + data: dict, + settings: provider_utils.Settings, +) -> models.ShipmentDetails: + shipment = None # parse carrier shipment type from "data" + label = "" # extract and process the shipment label to a valid base64 text + # invoice = "" # extract and process the shipment invoice to a valid base64 text if applies + + return models.ShipmentDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + tracking_number="", # extract tracking number from shipment + shipment_identifier="", # extract shipment identifier from shipment + label_type="PDF", # extract shipment label file format + docs=models.Documents( + label=label, # pass label base64 text + # invoice=invoice, # pass invoice base64 text if applies + ), + meta=dict( + # any relevent meta + ), + ) + + +def shipment_request( + payload: models.ShipmentRequest, + settings: provider_utils.Settings, +) -> lib.Serializable: + packages = lib.to_packages(payload.parcels) # preprocess the request parcels + service = provider_units.ShippingService.map(payload.service).value_or_key # preprocess the request services + options = lib.to_shipping_options( + payload.options, + package_options=packages.options, + ) # preprocess the request options + + request = None # map data to convert karrio model to freightcomv2 specific type + + return lib.Serializable(request) diff --git a/modules/connectors/freightcomv2/karrio/providers/freightcomv2/tracking.py b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/tracking.py new file mode 100644 index 0000000000..23729d1a3e --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/tracking.py @@ -0,0 +1,60 @@ + +import typing +import karrio.lib as lib +import karrio.core.units as units +import karrio.core.models as models +import karrio.providers.freightcomv2.error as error +import karrio.providers.freightcomv2.utils as provider_utils +import karrio.providers.freightcomv2.units as provider_units + + +def parse_tracking_response( + _response: lib.Deserializable[typing.List[typing.Tuple[str, dict]]], + settings: provider_utils.Settings, +) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]: + responses = _response.deserialize() + + messages = sum( + [ + error.parse_error_response(response, settings, tracking_number=_) + for _, response in responses + ], + start=[], + ) + tracking_details = [_extract_details(details, settings) for _, details in responses] + + return tracking_details, messages + + +def _extract_details( + data: dict, + settings: provider_utils.Settings, +) -> models.TrackingDetails: + tracking = None # parse carrier tracking object type + + return models.TrackingDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + tracking_number="", # extract tracking number from tracking + events=[ + models.TrackingEvent( + date=lib.fdate(""), # extract tracking event date + description="", # extract tracking event description or code + code="", # extract tracking event code + time=lib.ftime(""), # extract tracking event time + location="", # extract tracking event address + ) + for event in [] # extract tracking events + ], + estimated_delivery=lib.fdate(""), # extract tracking estimated date if provided + delivered=False, # compute tracking delivered status + ) + + +def tracking_request( + payload: models.TrackingRequest, + settings: provider_utils.Settings, +) -> lib.Serializable: + request = None # map data to convert karrio model to freightcomv2 specific type + + return lib.Serializable(request) diff --git a/modules/connectors/freightcomv2/karrio/providers/freightcomv2/units.py b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/units.py new file mode 100644 index 0000000000..dd9d5583e5 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/units.py @@ -0,0 +1,59 @@ + +import karrio.lib as lib +import karrio.core.units as units + + +class PackagingType(lib.StrEnum): + """ Carrier specific packaging type """ + PACKAGE = "PACKAGE" + + """ Unified Packaging type mapping """ + envelope = PACKAGE + pak = PACKAGE + tube = PACKAGE + pallet = PACKAGE + small_box = PACKAGE + medium_box = PACKAGE + your_packaging = PACKAGE + + +class ShippingService(lib.StrEnum): + """ Carrier specific services """ + freightcomv2_standard_service = "freightcom v2 Standard Service" + + +class ShippingOption(lib.Enum): + """ Carrier specific options """ + # freightcomv2_option = lib.OptionEnum("code") + + """ Unified Option type mapping """ + # insurance = freightcomv2_coverage # maps unified karrio option to carrier specific + + pass + + +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) + + +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/modules/connectors/freightcomv2/karrio/providers/freightcomv2/utils.py b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/utils.py new file mode 100644 index 0000000000..516f5c4e84 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/providers/freightcomv2/utils.py @@ -0,0 +1,29 @@ + +import karrio.core as core +import karrio.lib as lib +import karrio.providers.morneau.units as units + + +class Settings(core.Settings): + """freightcom v2 connection settings.""" + # username: str # carrier specific api credential key + apiKey: str = "mNlJ5Vwj5jn70YURDbksWyNdbrh08u24HnY0tJOn0Tz9wZdiCvfjktWDRXhFQtzb" + @property + def carrier_name(self): + return "freightcomv2" + + @property + def server_url(self): + return ( + "https://customer-external-api.ssd-test.freightcom.com" + if self.test_mode + else "https://external-api.freightcom.com" + ) + + # @property + # def connection_config(self) -> lib.units.Options: + # from karrio.providers.freightcomv2.units import ConnectionConfig + # return lib.to_connection_config( + # self.config or {}, + # option_type=ConnectionConfig, + # ) diff --git a/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/__init__.py b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/create_shipment_request.py b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/create_shipment_request.py new file mode 100644 index 0000000000..8f762da8b1 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/create_shipment_request.py @@ -0,0 +1,224 @@ +from attr import s +from typing import Optional, List +from jstruct import JStruct, JList + + +@s(auto_attribs=True) +class NumberType: + number: Optional[str] = None + extension: Optional[int] = None + + +@s(auto_attribs=True) +class BrokerType: + usecarrier: Optional[bool] = None + name: Optional[str] = None + accountnumber: Optional[str] = None + phonenumber: Optional[NumberType] = JStruct[NumberType] + faxnumber: Optional[NumberType] = JStruct[NumberType] + emailaddress: Optional[str] = None + usmcanumber: Optional[str] = None + fdanumber: Optional[str] = None + + +@s(auto_attribs=True) +class TotalCostType: + currency: Optional[str] = None + value: Optional[int] = None + + +@s(auto_attribs=True) +class WeightType: + unit: Optional[str] = None + value: Optional[float] = None + + +@s(auto_attribs=True) +class ProductType: + productname: Optional[str] = None + weight: Optional[WeightType] = JStruct[WeightType] + hscode: Optional[str] = None + countryoforigin: Optional[str] = None + numunits: Optional[int] = None + unitprice: Optional[TotalCostType] = JStruct[TotalCostType] + description: Optional[str] = None + + +@s(auto_attribs=True) +class AddressType: + addressline1: Optional[str] = None + addressline2: Optional[str] = None + unitnumber: Optional[str] = None + city: Optional[str] = None + region: Optional[str] = None + country: Optional[str] = None + postalcode: Optional[str] = None + + +@s(auto_attribs=True) +class TaxRecipientType: + type: Optional[str] = None + shippertaxidentifier: Optional[str] = None + receivertaxidentifier: Optional[str] = None + thirdpartytaxidentifier: Optional[str] = None + othertaxidentifier: Optional[str] = None + name: Optional[str] = None + address: Optional[AddressType] = JStruct[AddressType] + phonenumber: Optional[NumberType] = JStruct[NumberType] + reasonforexport: Optional[str] = None + additionalremarks: Optional[str] = None + comments: Optional[str] = None + + +@s(auto_attribs=True) +class CustomsInvoiceDetailsType: + taxrecipient: Optional[TaxRecipientType] = JStruct[TaxRecipientType] + products: List[ProductType] = JList[ProductType] + + +@s(auto_attribs=True) +class CustomsInvoiceType: + source: Optional[str] = None + broker: Optional[BrokerType] = JStruct[BrokerType] + details: Optional[CustomsInvoiceDetailsType] = JStruct[CustomsInvoiceDetailsType] + + +@s(auto_attribs=True) +class ReadyType: + hour: Optional[int] = None + minute: Optional[int] = None + + +@s(auto_attribs=True) +class DestinationType: + name: Optional[str] = None + address: Optional[AddressType] = JStruct[AddressType] + residential: Optional[bool] = None + tailgaterequired: Optional[bool] = None + instructions: Optional[str] = None + contactname: Optional[str] = None + phonenumber: Optional[NumberType] = JStruct[NumberType] + emailaddresses: List[str] = [] + receivesemailupdates: Optional[bool] = None + readyat: Optional[ReadyType] = JStruct[ReadyType] + readyuntil: Optional[ReadyType] = JStruct[ReadyType] + signaturerequirement: Optional[str] = None + + +@s(auto_attribs=True) +class DateType: + year: Optional[int] = None + month: Optional[int] = None + day: Optional[int] = None + + +@s(auto_attribs=True) +class InsuranceType: + type: Optional[str] = None + totalcost: Optional[TotalCostType] = JStruct[TotalCostType] + + +@s(auto_attribs=True) +class DangerousGoodsDetailsType: + packaginggroup: Optional[str] = None + goodsclass: Optional[str] = None + description: Optional[str] = None + unitednationsnumber: Optional[str] = None + emergencycontactname: Optional[str] = None + emergencycontactphonenumber: Optional[NumberType] = JStruct[NumberType] + + +@s(auto_attribs=True) +class CuboidType: + unit: Optional[str] = None + l: Optional[int] = None + w: Optional[int] = None + h: Optional[int] = None + + +@s(auto_attribs=True) +class MeasurementsType: + weight: Optional[WeightType] = JStruct[WeightType] + cuboid: Optional[CuboidType] = JStruct[CuboidType] + + +@s(auto_attribs=True) +class PalletType: + measurements: Optional[MeasurementsType] = JStruct[MeasurementsType] + description: Optional[str] = None + freightclass: Optional[str] = None + nmfc: Optional[str] = None + contentstype: Optional[str] = None + numpieces: Optional[int] = None + + +@s(auto_attribs=True) +class InBondDetailsType: + type: Optional[str] = None + name: Optional[str] = None + address: Optional[str] = None + contactmethod: Optional[str] = None + contactemailaddress: Optional[str] = None + contactphonenumber: Optional[NumberType] = JStruct[NumberType] + + +@s(auto_attribs=True) +class PalletServiceDetailsType: + limitedaccessdeliverytype: Optional[str] = None + limitedaccessdeliveryothername: Optional[str] = None + inbond: Optional[bool] = None + inbonddetails: Optional[InBondDetailsType] = JStruct[InBondDetailsType] + appointmentdelivery: Optional[bool] = None + protectfromfreeze: Optional[bool] = None + thresholdpickup: Optional[bool] = None + thresholddelivery: Optional[bool] = None + + +@s(auto_attribs=True) +class PackagingPropertiesType: + pallettype: Optional[str] = None + hasstackablepallets: Optional[bool] = None + dangerousgoods: Optional[str] = None + dangerousgoodsdetails: Optional[DangerousGoodsDetailsType] = JStruct[DangerousGoodsDetailsType] + pallets: List[PalletType] = JList[PalletType] + palletservicedetails: Optional[PalletServiceDetailsType] = JStruct[PalletServiceDetailsType] + + +@s(auto_attribs=True) +class CreateShipmentRequestDetailsType: + origin: Optional[DestinationType] = JStruct[DestinationType] + destination: Optional[DestinationType] = JStruct[DestinationType] + expectedshipdate: Optional[DateType] = JStruct[DateType] + packagingtype: Optional[str] = None + packagingproperties: Optional[PackagingPropertiesType] = JStruct[PackagingPropertiesType] + insurance: Optional[InsuranceType] = JStruct[InsuranceType] + referencecodes: List[str] = [] + + +@s(auto_attribs=True) +class DispatchDetailsType: + date: Optional[DateType] = JStruct[DateType] + readyat: Optional[ReadyType] = JStruct[ReadyType] + readyuntil: Optional[ReadyType] = JStruct[ReadyType] + + +@s(auto_attribs=True) +class PickupDetailsType: + prescheduledpickup: Optional[bool] = None + date: Optional[DateType] = JStruct[DateType] + readyat: Optional[ReadyType] = JStruct[ReadyType] + readyuntil: Optional[ReadyType] = JStruct[ReadyType] + pickuplocation: Optional[str] = None + contactname: Optional[str] = None + contactphonenumber: Optional[NumberType] = JStruct[NumberType] + + +@s(auto_attribs=True) +class CreateShipmentRequestType: + uniqueid: Optional[str] = None + paymentmethodid: Optional[str] = None + serviceid: Optional[str] = None + details: Optional[CreateShipmentRequestDetailsType] = JStruct[CreateShipmentRequestDetailsType] + customsinvoice: Optional[CustomsInvoiceType] = JStruct[CustomsInvoiceType] + pickupdetails: Optional[PickupDetailsType] = JStruct[PickupDetailsType] + dispatchdetails: Optional[DispatchDetailsType] = JStruct[DispatchDetailsType] diff --git a/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/create_shipment_response.py b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/create_shipment_response.py new file mode 100644 index 0000000000..f20f629679 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/create_shipment_response.py @@ -0,0 +1,8 @@ +from attr import s +from typing import Optional + + +@s(auto_attribs=True) +class CreateShipmentResponseType: + id: Optional[str] = None + previouslycreated: Optional[bool] = None diff --git a/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/error.py b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/error.py new file mode 100644 index 0000000000..cddf39c047 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/error.py @@ -0,0 +1,6 @@ +from attr import s + + +@s(auto_attribs=True) +class ErrorType: + pass diff --git a/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/rate_request.py b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/rate_request.py new file mode 100644 index 0000000000..4ea967b931 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/rate_request.py @@ -0,0 +1,151 @@ +from attr import s +from typing import Optional, List +from jstruct import JStruct, JList + + +@s(auto_attribs=True) +class AddressType: + addressline1: Optional[str] = None + addressline2: Optional[str] = None + unitnumber: Optional[str] = None + city: Optional[str] = None + region: Optional[str] = None + country: Optional[str] = None + postalcode: Optional[str] = None + + +@s(auto_attribs=True) +class PhoneNumberType: + number: Optional[str] = None + extension: Optional[int] = None + + +@s(auto_attribs=True) +class ReadyType: + hour: Optional[int] = None + minute: Optional[int] = None + + +@s(auto_attribs=True) +class DestinationType: + name: Optional[str] = None + address: Optional[AddressType] = JStruct[AddressType] + residential: Optional[bool] = None + tailgaterequired: Optional[bool] = None + instructions: Optional[str] = None + contactname: Optional[str] = None + phonenumber: Optional[PhoneNumberType] = JStruct[PhoneNumberType] + emailaddresses: List[str] = [] + receivesemailupdates: Optional[bool] = None + readyat: Optional[ReadyType] = JStruct[ReadyType] + readyuntil: Optional[ReadyType] = JStruct[ReadyType] + signaturerequirement: Optional[str] = None + + +@s(auto_attribs=True) +class ExpectedShipDateType: + year: Optional[int] = None + month: Optional[int] = None + day: Optional[int] = None + + +@s(auto_attribs=True) +class TotalCostType: + currency: Optional[str] = None + value: Optional[int] = None + + +@s(auto_attribs=True) +class InsuranceType: + type: Optional[str] = None + totalcost: Optional[TotalCostType] = JStruct[TotalCostType] + + +@s(auto_attribs=True) +class DangerousGoodsDetailsType: + packaginggroup: Optional[str] = None + goodsclass: Optional[str] = None + description: Optional[str] = None + unitednationsnumber: Optional[str] = None + emergencycontactname: Optional[str] = None + emergencycontactphonenumber: Optional[PhoneNumberType] = JStruct[PhoneNumberType] + + +@s(auto_attribs=True) +class CuboidType: + unit: Optional[str] = None + l: Optional[int] = None + w: Optional[int] = None + h: Optional[int] = None + + +@s(auto_attribs=True) +class WeightType: + unit: Optional[str] = None + value: Optional[float] = None + + +@s(auto_attribs=True) +class MeasurementsType: + weight: Optional[WeightType] = JStruct[WeightType] + cuboid: Optional[CuboidType] = JStruct[CuboidType] + + +@s(auto_attribs=True) +class PalletType: + measurements: Optional[MeasurementsType] = JStruct[MeasurementsType] + description: Optional[str] = None + freightclass: Optional[str] = None + nmfc: Optional[str] = None + contentstype: Optional[str] = None + numpieces: Optional[int] = None + + +@s(auto_attribs=True) +class InBondDetailsType: + type: Optional[str] = None + name: Optional[str] = None + address: Optional[str] = None + contactmethod: Optional[str] = None + contactemailaddress: Optional[str] = None + contactphonenumber: Optional[PhoneNumberType] = JStruct[PhoneNumberType] + + +@s(auto_attribs=True) +class PalletServiceDetailsType: + limitedaccessdeliverytype: Optional[str] = None + limitedaccessdeliveryothername: Optional[str] = None + inbond: Optional[bool] = None + inbonddetails: Optional[InBondDetailsType] = JStruct[InBondDetailsType] + appointmentdelivery: Optional[bool] = None + protectfromfreeze: Optional[bool] = None + thresholdpickup: Optional[bool] = None + thresholddelivery: Optional[bool] = None + + +@s(auto_attribs=True) +class PackagingPropertiesType: + pallettype: Optional[str] = None + hasstackablepallets: Optional[bool] = None + dangerousgoods: Optional[str] = None + dangerousgoodsdetails: Optional[DangerousGoodsDetailsType] = JStruct[DangerousGoodsDetailsType] + pallets: List[PalletType] = JList[PalletType] + palletservicedetails: Optional[PalletServiceDetailsType] = JStruct[PalletServiceDetailsType] + + +@s(auto_attribs=True) +class DetailsType: + origin: Optional[DestinationType] = JStruct[DestinationType] + destination: Optional[DestinationType] = JStruct[DestinationType] + expectedshipdate: Optional[ExpectedShipDateType] = JStruct[ExpectedShipDateType] + packagingtype: Optional[str] = None + packagingproperties: Optional[PackagingPropertiesType] = JStruct[PackagingPropertiesType] + insurance: Optional[InsuranceType] = JStruct[InsuranceType] + referencecodes: List[str] = [] + + +@s(auto_attribs=True) +class RateRequestType: + services: List[str] = [] + excludedservices: List[str] = [] + details: Optional[DetailsType] = JStruct[DetailsType] diff --git a/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/rate_response.py b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/rate_response.py new file mode 100644 index 0000000000..54ab809cf8 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/rate_response.py @@ -0,0 +1,7 @@ +from attr import s +from typing import Optional + + +@s(auto_attribs=True) +class RateResponseType: + requestid: Optional[str] = None diff --git a/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/tracking_response.py b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/tracking_response.py new file mode 100644 index 0000000000..f1fceca3e7 --- /dev/null +++ b/modules/connectors/freightcomv2/karrio/schemas/freightcomv2/tracking_response.py @@ -0,0 +1,23 @@ +from attr import s +from typing import Optional, List +from jstruct import JStruct, JList + + +@s(auto_attribs=True) +class WhereType: + city: Optional[str] = None + region: Optional[str] = None + country: Optional[str] = None + + +@s(auto_attribs=True) +class EventType: + type: Optional[str] = None + when: Optional[str] = None + where: Optional[WhereType] = JStruct[WhereType] + message: Optional[str] = None + + +@s(auto_attribs=True) +class TrackingResponseType: + events: List[EventType] = JList[EventType] diff --git a/modules/connectors/freightcomv2/schemas/create_shipment_request.json b/modules/connectors/freightcomv2/schemas/create_shipment_request.json new file mode 100644 index 0000000000..a4959ee704 --- /dev/null +++ b/modules/connectors/freightcomv2/schemas/create_shipment_request.json @@ -0,0 +1,236 @@ +{ + "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 + } + ], + "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/modules/connectors/freightcomv2/schemas/create_shipment_response.json b/modules/connectors/freightcomv2/schemas/create_shipment_response.json new file mode 100644 index 0000000000..bc23e0725a --- /dev/null +++ b/modules/connectors/freightcomv2/schemas/create_shipment_response.json @@ -0,0 +1,4 @@ +{ + "id": "string", + "previously_created": true +} diff --git a/modules/connectors/freightcomv2/schemas/error.json b/modules/connectors/freightcomv2/schemas/error.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/modules/connectors/freightcomv2/schemas/error.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/modules/connectors/freightcomv2/schemas/rate_request.json b/modules/connectors/freightcomv2/schemas/rate_request.json new file mode 100644 index 0000000000..5c17cbe65f --- /dev/null +++ b/modules/connectors/freightcomv2/schemas/rate_request.json @@ -0,0 +1,140 @@ +{ + "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 + } + ], + "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" + ] + } +} diff --git a/modules/connectors/freightcomv2/schemas/rate_response.json b/modules/connectors/freightcomv2/schemas/rate_response.json new file mode 100644 index 0000000000..a427feb0eb --- /dev/null +++ b/modules/connectors/freightcomv2/schemas/rate_response.json @@ -0,0 +1,3 @@ +{ + "request_id": "string" +} diff --git a/modules/connectors/freightcomv2/schemas/tracking_response.json b/modules/connectors/freightcomv2/schemas/tracking_response.json new file mode 100644 index 0000000000..e6a344a58a --- /dev/null +++ b/modules/connectors/freightcomv2/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/modules/connectors/freightcomv2/setup.py b/modules/connectors/freightcomv2/setup.py new file mode 100644 index 0000000000..c8539e5fb8 --- /dev/null +++ b/modules/connectors/freightcomv2/setup.py @@ -0,0 +1,27 @@ + +"""Warning: This setup.py is only there for git install until poetry support git subdirectory""" +from setuptools import setup, find_namespace_packages + +with open("README.md", "r") as fh: + long_description = fh.read() + +setup( + name="karrio.freightcomv2", + version="2024.6", + description="Karrio - freightcom v2 Shipping Extension", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/karrioapi/karrio", + author="karrio", + author_email="hello@karrio.io", + license="Apache-2.0", + packages=find_namespace_packages(exclude=["tests.*", "tests"]), + install_requires=["karrio"], + classifiers=[ + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + ], + zip_safe=False, + include_package_data=True, +) diff --git a/modules/connectors/freightcomv2/tests/__init__.py b/modules/connectors/freightcomv2/tests/__init__.py new file mode 100644 index 0000000000..a1623f476b --- /dev/null +++ b/modules/connectors/freightcomv2/tests/__init__.py @@ -0,0 +1,4 @@ + +from tests.freightcomv2.test_rate import * +from tests.freightcomv2.test_tracking import * +from tests.freightcomv2.test_shipment import * \ No newline at end of file diff --git a/modules/connectors/freightcomv2/tests/freightcomv2/__init__.py b/modules/connectors/freightcomv2/tests/freightcomv2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/connectors/freightcomv2/tests/freightcomv2/fixture.py b/modules/connectors/freightcomv2/tests/freightcomv2/fixture.py new file mode 100644 index 0000000000..10105f3492 --- /dev/null +++ b/modules/connectors/freightcomv2/tests/freightcomv2/fixture.py @@ -0,0 +1,8 @@ + +import karrio + +gateway = karrio.gateway["freightcomv2"].create( + dict( + # add required carrier API setting key/value here + ) +) diff --git a/modules/connectors/freightcomv2/tests/freightcomv2/test_rate.py b/modules/connectors/freightcomv2/tests/freightcomv2/test_rate.py new file mode 100644 index 0000000000..68258b0e98 --- /dev/null +++ b/modules/connectors/freightcomv2/tests/freightcomv2/test_rate.py @@ -0,0 +1,51 @@ + +import unittest +from unittest.mock import patch, ANY +from .fixture import gateway + +import karrio +import karrio.lib as lib +import karrio.core.models as models + + +class Testfreightcomv2Rating(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(request.serialize(), RateRequest) + + def test_get_rate(self): + with patch("karrio.mappers.freightcomv2.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}", + ) + + def test_parse_rate_response(self): + with patch("karrio.mappers.freightcomv2.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) + + +if __name__ == "__main__": + unittest.main() + + +RatePayload = {} + +ParsedRateResponse = [] + + +RateRequest = {} + +RateResponse = """{} +""" diff --git a/modules/connectors/freightcomv2/tests/freightcomv2/test_shipment.py b/modules/connectors/freightcomv2/tests/freightcomv2/test_shipment.py new file mode 100644 index 0000000000..42205f9908 --- /dev/null +++ b/modules/connectors/freightcomv2/tests/freightcomv2/test_shipment.py @@ -0,0 +1,93 @@ + +import unittest +from unittest.mock import patch, ANY +from .fixture import gateway + +import karrio +import karrio.lib as lib +import karrio.core.models as models + + +class Testfreightcomv2Shipping(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(request.serialize(), ShipmentRequest) + + def test_create_cancel_shipment_request(self): + request = gateway.mapper.create_cancel_shipment_request( + self.ShipmentCancelRequest + ) + + self.assertEqual(request.serialize(), ShipmentCancelRequest) + + def test_create_shipment(self): + with patch("karrio.mappers.freightcomv2.proxy.lib.request") as mock: + mock.return_value = "{}" + karrio.Shipment.create(self.ShipmentRequest).from_(gateway) + + self.assertEqual( + mock.call_args[1]["url"], + f"{gateway.settings.server_url}", + ) + + def test_cancel_shipment(self): + with patch("karrio.mappers.freightcomv2.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}", + ) + + def test_parse_shipment_response(self): + with patch("karrio.mappers.freightcomv2.proxy.lib.request") as mock: + mock.return_value = ShipmentResponse + parsed_response = ( + karrio.Shipment.create(self.ShipmentRequest).from_(gateway).parse() + ) + + self.assertListEqual(lib.to_dict(parsed_response), ParsedShipmentResponse) + + def test_parse_cancel_shipment_response(self): + with patch("karrio.mappers.freightcomv2.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), ParsedCancelShipmentResponse + ) + + +if __name__ == "__main__": + unittest.main() + + +ShipmentPayload = {} + +ShipmentCancelPayload = { + "shipment_identifier": "794947717776", +} + +ParsedShipmentResponse = [] + +ParsedCancelShipmentResponse = [] + + +ShipmentRequest = {} + +ShipmentCancelRequest = {} + +ShipmentResponse = """{} +""" + +ShipmentCancelResponse = """{} +""" diff --git a/modules/connectors/freightcomv2/tests/freightcomv2/test_tracking.py b/modules/connectors/freightcomv2/tests/freightcomv2/test_tracking.py new file mode 100644 index 0000000000..a6ee77b16c --- /dev/null +++ b/modules/connectors/freightcomv2/tests/freightcomv2/test_tracking.py @@ -0,0 +1,73 @@ + +import unittest +from unittest.mock import patch, ANY +from .fixture import gateway + +import karrio +import karrio.lib as lib +import karrio.core.models as models + + +class Testfreightcomv2Tracking(unittest.TestCase): + def setUp(self): + self.maxDiff = None + self.TrackingRequest = models.TrackingRequest(**TrackingPayload) + + def test_create_tracking_request(self): + request = gateway.mapper.create_tracking_request(self.TrackingRequest) + + self.assertEqual(request.serialize(), TrackingRequest) + + def test_get_tracking(self): + with patch("karrio.mappers.freightcomv2.proxy.lib.request") as mock: + mock.return_value = "{}" + karrio.Tracking.fetch(self.TrackingRequest).from_(gateway) + + self.assertEqual( + mock.call_args[1]["url"], + f"{gateway.settings.server_url}", + ) + + def test_parse_tracking_response(self): + with patch("karrio.mappers.freightcomv2.proxy.lib.request") as mock: + mock.return_value = TrackingResponse + parsed_response = ( + karrio.Tracking.fetch(self.TrackingRequest).from_(gateway).parse() + ) + + self.assertListEqual( + lib.to_dict(parsed_response), ParsedTrackingResponse + ) + + def test_parse_error_response(self): + with patch("karrio.mappers.freightcomv2.proxy.lib.request") as mock: + mock.return_value = ErrorResponse + parsed_response = ( + karrio.Tracking.fetch(self.TrackingRequest).from_(gateway).parse() + ) + + self.assertListEqual( + lib.to_dict(parsed_response), ParsedErrorResponse + ) + + +if __name__ == "__main__": + unittest.main() + + +TrackingPayload = { + "tracking_numbers": ["89108749065090"], +} + +ParsedTrackingResponse = [] + +ParsedErrorResponse = [] + + +TrackingRequest = {} + +TrackingResponse = """{} +""" + +ErrorResponse = """{} +""" diff --git a/modules/connectors/morneau/README.md b/modules/connectors/morneau/README.md new file mode 100644 index 0000000000..7758d9080d --- /dev/null +++ b/modules/connectors/morneau/README.md @@ -0,0 +1,31 @@ + +# karrio.morneau + +This package is a Groupe Morneau extension of the [karrio](https://pypi.org/project/karrio) multi carrier shipping SDK. + +## Requirements + +`Python 3.7+` + +## Installation + +```bash +pip install karrio.morneau +``` + +## Usage + +```python +import karrio +from karrio.mappers.morneau.settings import Settings + + +# Initialize a carrier gateway +morneau = karrio.gateway["morneau"].create( + Settings( + ... + ) +) +``` + +Check the [Karrio Mutli-carrier SDK docs](https://docs.karrio.io) for Shipping API requests diff --git a/modules/connectors/morneau/generate b/modules/connectors/morneau/generate new file mode 100755 index 0000000000..d96d2e4726 --- /dev/null +++ b/modules/connectors/morneau/generate @@ -0,0 +1,21 @@ +SCHEMAS=./schemas +LIB_MODULES=./karrio/schemas/morneau +find "${LIB_MODULES}" -name "*.py" -exec rm -r {} \; +touch "${LIB_MODULES}/__init__.py" + +quicktype () { + echo "Generating $1..." + docker run -it -v $PWD:/app -e SCHEMAS=/app/schemas -e LIB_MODULES=/app/karrio/schemas/morneau \ + karrio/tools /quicktype/script/quicktype --no-uuids --no-date-times --no-enums --src-lang json --lang jstruct \ + --no-nice-property-names --all-properties-optional --type-as-suffix $@ +} + +quicktype --src="${SCHEMAS}/error.json" --out="${LIB_MODULES}/error.py" +quicktype --src="${SCHEMAS}/error_cancel_shipment.json" --out="${LIB_MODULES}/error_cancel_shipment.py" +quicktype --src="${SCHEMAS}/error_shipment.json" --out="${LIB_MODULES}/error_shipment.py" +quicktype --src="${SCHEMAS}/rate_request.json" --out="${LIB_MODULES}/rate_request.py" +quicktype --src="${SCHEMAS}/rate_response.json" --out="${LIB_MODULES}/rate_response.py" +quicktype --src="${SCHEMAS}/shipment_purchase_request.json" --out="${LIB_MODULES}/shipment_purchase_request.py" +quicktype --src="${SCHEMAS}/shipment_purchase_response.json" --out="${LIB_MODULES}/shipment_purchase_response.py" +quicktype --src="${SCHEMAS}/error_tracking.json" --out="${LIB_MODULES}/error_tracking.py" +quicktype --src="${SCHEMAS}/trackers_response.json" --out="${LIB_MODULES}/trackers_response.py" diff --git a/modules/connectors/morneau/karrio/mappers/morneau/__init__.py b/modules/connectors/morneau/karrio/mappers/morneau/__init__.py new file mode 100644 index 0000000000..e7f68731ef --- /dev/null +++ b/modules/connectors/morneau/karrio/mappers/morneau/__init__.py @@ -0,0 +1,19 @@ + +from karrio.core.metadata import Metadata + +from karrio.mappers.morneau.mapper import Mapper +from karrio.mappers.morneau.proxy import Proxy +from karrio.mappers.morneau.settings import Settings +import karrio.providers.morneau.units as units + + +METADATA = Metadata( + id="morneau", + label="Groupe Morneau", + # Integrations + Mapper=Mapper, + Proxy=Proxy, + Settings=Settings, + # Data Units + is_hub=False +) diff --git a/modules/connectors/morneau/karrio/mappers/morneau/mapper.py b/modules/connectors/morneau/karrio/mappers/morneau/mapper.py new file mode 100644 index 0000000000..bc3fc9edb5 --- /dev/null +++ b/modules/connectors/morneau/karrio/mappers/morneau/mapper.py @@ -0,0 +1,53 @@ +"""Karrio Groupe Morneau client mapper.""" + +import typing + +import karrio.api.mapper as mapper +import karrio.core.models as models +import karrio.lib as lib +import karrio.mappers.morneau.settings as provider_settings +import karrio.providers.morneau as provider + + +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/modules/connectors/morneau/karrio/mappers/morneau/proxy.py b/modules/connectors/morneau/karrio/mappers/morneau/proxy.py new file mode 100644 index 0000000000..d07774dcf3 --- /dev/null +++ b/modules/connectors/morneau/karrio/mappers/morneau/proxy.py @@ -0,0 +1,85 @@ +"""Karrio Groupe Morneau client proxy.""" +import typing + +import karrio.api.proxy as proxy +import karrio.lib as lib +import karrio.mappers.morneau.settings as provider_settings + + +class Proxy(proxy.Proxy): + settings: provider_settings.Settings + + def get_rates(self, request: lib.Serializable) -> lib.Deserializable[str]: + # Send request for quotation + response = lib.request( + url=f"{self.settings.rates_server_url}/quotes/add", + data=lib.to_json(request.serialize()), + trace=self.trace_as("json"), + method="POST", + headers={ + "Authorization": f"Bearer {self.settings.rating_jwt_token}", + "Content-Type": "application/json", + }, + ) + print(self.settings.rating_jwt_token) + print(response) + return lib.Deserializable(response, lib.to_dict) + + def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]: + response = lib.request( + url=f"{self.settings.server_url}/LoadTender/{self.settings.caller_id}", + data=lib.to_json(request.serialize()), + trace=self.trace_as("json"), + method="POST", + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + "X-API-VERSION": "1", + "Authorization": f"Bearer {self.settings.shipment_jwt_token}" + }, + ) + + return lib.Deserializable(response, lib.to_dict) + + def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]: + payload = request.serialize() + response = lib.request( + url=f"{self.settings.server_url}/LoadTender/{self.settings.caller_id}/{payload['reference']}/cancel", + method="GET", + headers={ + "Accept": "application/json", + "X-API-VERSION": "1", + "Authorization": f"Bearer {self.settings.shipment_jwt_token}" + }, + # on_error=provider_error.parse_http_response, + + ) + + return lib.Deserializable(response if any(response) else "{}", lib.to_dict) + + def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[str]: + def _get_tracking(reference: str): + return reference, lib.request( + url=f"{self.settings.tracking_url}/api/v1/tracking/en/MORNEAU/{reference}", + trace=self.trace_as("json"), + method="GET", + headers={ + "Content-Type": "application/json", + "Accept": "application/json", + "X-API-VERSION": "1", + "Authorization": f"Bearer {self.settings.tracking_jwt_token}" + }, + ) + + responses: typing.List[typing.Tuple[str, str]] = lib.run_concurently( + _get_tracking, request.serialize() + ) + + return lib.Deserializable( + responses, + lambda res: [ + (num, lib.to_dict(track)) + for num, track in res + if any(track.strip()) + ], + ) diff --git a/modules/connectors/morneau/karrio/mappers/morneau/settings.py b/modules/connectors/morneau/karrio/mappers/morneau/settings.py new file mode 100644 index 0000000000..340f33e637 --- /dev/null +++ b/modules/connectors/morneau/karrio/mappers/morneau/settings.py @@ -0,0 +1,24 @@ +"""Karrio Groupe Morneau client settings.""" + +import attr +import jstruct +import karrio.lib as lib +from karrio.providers.morneau.utils import Settings as BaseSettings + + +@attr.s(auto_attribs=True) +class Settings(BaseSettings): + """Groupe Morneau connection settings.""" + + username: str + password: str + caller_id: str + billed_id: int + division: str = "Morneau" + carrier_id: str = "morneau" + account_country_code: str = None + cache: lib.Cache = jstruct.JStruct[lib.Cache, False, dict(default=lib.Cache())] + test_mode: bool = False + metadata: dict = {} + id: str = None + config: dict = {} diff --git a/modules/connectors/morneau/karrio/providers/morneau/__init__.py b/modules/connectors/morneau/karrio/providers/morneau/__init__.py new file mode 100644 index 0000000000..a9bd678f8b --- /dev/null +++ b/modules/connectors/morneau/karrio/providers/morneau/__init__.py @@ -0,0 +1,13 @@ + +from karrio.providers.morneau.utils import Settings +from karrio.providers.morneau.rate import parse_rate_response, rate_request +from karrio.providers.morneau.shipment import ( + parse_shipment_cancel_response, + parse_shipment_response, + shipment_cancel_request, + shipment_request, +) +from karrio.providers.morneau.tracking import ( + parse_tracking_response, + tracking_request, +) diff --git a/modules/connectors/morneau/karrio/providers/morneau/error.py b/modules/connectors/morneau/karrio/providers/morneau/error.py new file mode 100644 index 0000000000..5ebc11c83d --- /dev/null +++ b/modules/connectors/morneau/karrio/providers/morneau/error.py @@ -0,0 +1,69 @@ +import typing +import urllib.error + +import karrio.core.models as models +import karrio.lib as lib +import karrio.providers.morneau.utils as provider_utils +from karrio.core.utils import DP +from karrio.schemas.morneau.error import ErrorType +from karrio.schemas.morneau.error_cancel_shipment import ErrorCancelShipmentType +from karrio.schemas.morneau.error_tracking import ErrorTrackingType + + +def parse_error_response( + response: dict, + settings: provider_utils.Settings, + **kwargs, +) -> typing.List[models.Message]: + if isinstance(response.get("Message", None), str): + errors_model = [DP.to_object(ErrorCancelShipmentType, response)] + elif response.get("GenericDetail", None): + errors_model = [DP.to_object(ErrorType, response)] + else: + errors_model = [DP.to_object(ErrorTrackingType, response)] + + errors = (errors_model + if response.get("Message", None) or response.get("GenericDetail", None) is not None + else []) + + is_GenericDetail = True if response.get("Message", "") or response.get("GenericDetail", "") else False + + messages = [] + for error in errors: + # Determine the code + if response.get("GenericDetail", None): + code = error.GenericDetail.QuoteNumber + message = error.FailedValidation + elif isinstance(response.get("Message", None), dict): + code = error.Message.Code + message = error.Message.ErrorMessage + else: + code = "402" + message = error.Message + + # Create the message model + message_model = models.Message( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + code=code, + message=message, + details={**kwargs}, + ) + + messages.append(message_model) + + return messages + + +def parse_http_response(response: urllib.error.HTTPError) -> dict: + try: + return lib.decode(response.read()) + except Exception: + pass + + return lib.to_json( + { + "error-code": response.code, + "message": response.reason, + } + ) diff --git a/modules/connectors/morneau/karrio/providers/morneau/rate.py b/modules/connectors/morneau/karrio/providers/morneau/rate.py new file mode 100644 index 0000000000..a9be1c8c4c --- /dev/null +++ b/modules/connectors/morneau/karrio/providers/morneau/rate.py @@ -0,0 +1,89 @@ +import typing + +import karrio.core.models as models +import karrio.lib as lib +import karrio.providers.morneau.error as error +import karrio.providers.morneau.utils as provider_utils + + +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() + + # Check for errors in the response + if "FailedValidation" in response or "GenericDetail" in response: + messages = [error.ErrorType(response).FailedValidation] # Adapt based on actual error structure + return [], messages + + # If response is successful, extract rate details + rate_details = _extract_details(response, settings) + + print(rate_details) + + return [rate_details], [] + + +def _extract_details( + data: dict, + settings: provider_utils.Settings, +) -> models.RateDetails: + # Extract necessary details from the rate response + total_charge = data.get("TotalCharges", 0.0) + currency = "CAD" # Assuming currency is CAD, update as needed + service = data.get("Standard", "Regular") # Update with actual service key, if available + transit_days = 0 # Transit days key unknown, update if available in response + + print("total_charge", total_charge) + print("currency", currency) + print("service", service) + print("transit_days", transit_days) + + rate_details = models.RateDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + service=service, + total_charge=total_charge, + currency=currency, + transit_days=transit_days, + + ) + print(rate_details) + return rate_details + + +def rate_request( + payload: models.RateRequest, + settings: provider_utils.Settings, +) -> lib.Serializable: + packages = lib.to_packages(payload.parcels) # preprocess the request parcels + commodities = payload.parcels + + request_payload = { + "BillToCodeId": settings.billed_id, + "Division": settings.division, + "Quote": { + "StartZone": payload.shipper.postal_code[:3] + " " + payload.shipper.postal_code[3:], + "EndZone": payload.recipient.postal_code[:3] + " " + payload.recipient.postal_code[3:], + "UserName": settings.username, + "NbPallet": len(packages), # Assuming one parcel per pallet + "Weight": float(sum(package.weight.value for package in packages)), + "WeightUnit": payload.parcels[0].weight_unit, + # "Commodities": [{ + # "Piece": 1, + # "Length": float(package.length.value), + # "Width": float(package.width.value), + # "Height": float(package.height.value) + # } for package in packages], + "Commodities": ['RENDEZVOUS', 'PCAMLIVR', 'HOME'], + "Dimensions": [{ + "Piece": 1, + "Length": float(package.length.value), + "Width": float(package.width.value), + "Height": float(package.height.value) + } for package in packages] + } + } + + return lib.Serializable(request_payload, lib.to_dict) diff --git a/modules/connectors/morneau/karrio/providers/morneau/shipment/__init__.py b/modules/connectors/morneau/karrio/providers/morneau/shipment/__init__.py new file mode 100644 index 0000000000..e1289929e1 --- /dev/null +++ b/modules/connectors/morneau/karrio/providers/morneau/shipment/__init__.py @@ -0,0 +1,9 @@ + +from karrio.providers.morneau.shipment.create import ( + parse_shipment_response, + shipment_request, +) +from karrio.providers.morneau.shipment.cancel import ( + parse_shipment_cancel_response, + shipment_cancel_request, +) diff --git a/modules/connectors/morneau/karrio/providers/morneau/shipment/cancel.py b/modules/connectors/morneau/karrio/providers/morneau/shipment/cancel.py new file mode 100644 index 0000000000..e70aac8e0a --- /dev/null +++ b/modules/connectors/morneau/karrio/providers/morneau/shipment/cancel.py @@ -0,0 +1,31 @@ +import typing + +import karrio.core.models as models +import karrio.lib as lib +import karrio.providers.morneau.error as error +import karrio.providers.morneau.utils as provider_utils + + +def parse_shipment_cancel_response( + response: lib.Deserializable[dict], + settings: provider_utils.Settings, +) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]: + response_messages: list = [] # extract carrier response errors and messages + messages = error.parse_error_response({}, settings) + success = len(messages) == 0 + + 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 shipment_cancel_request(payload: models.ShipmentCancelRequest, _) -> lib.Serializable: + request = dict(reference=payload.shipment_identifier) + return lib.Serializable(request) diff --git a/modules/connectors/morneau/karrio/providers/morneau/shipment/create.py b/modules/connectors/morneau/karrio/providers/morneau/shipment/create.py new file mode 100644 index 0000000000..fd05b2df9c --- /dev/null +++ b/modules/connectors/morneau/karrio/providers/morneau/shipment/create.py @@ -0,0 +1,168 @@ +import typing + +import karrio.core.models as models +import karrio.lib as lib +import karrio.providers.morneau.error as provider_error +import karrio.providers.morneau.utils as provider_utils +import karrio.schemas.morneau.shipment_purchase_response as shipping + + +def parse_shipment_response( + response: lib.Deserializable[dict], + settings: provider_utils.Settings, +) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]: + response_dict = response.deserialize() + + errors = provider_error.parse_error_response(response_dict, settings) + shipment = _extract_details(response_dict, settings) if "error" not in response_dict else None + + return shipment, errors + + +def _extract_details( + data: dict, + settings: provider_utils.Settings, +) -> models.ShipmentDetails: + shipment = lib.to_object(shipping.ShipmentPurchaseResponseType, data) + shipment_identifier = shipment.ShipmentIdentifier + + # Assuming the first LoadTenderConfirmation contains the primary details + load_tender_confirmation = shipment.LoadTenderConfirmations[0] + + freight_bill_number = load_tender_confirmation.FreightBillNumber + status = load_tender_confirmation.Status + is_accepted = load_tender_confirmation.IsAccepted + + label = "" # Placeholder: replace with actual label extraction logic if applicable + tracking_number = freight_bill_number # Assuming the FreightBillNumber is used as the tracking number + + meta = { + "status": status, + "is_accepted": is_accepted + } + + return models.ShipmentDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + tracking_number=tracking_number, + shipment_identifier=shipment_identifier, + label_type="PDF", + docs=models.Documents(label=""), + meta=meta, + ) + + +def shipment_request( + payload: models.ShipmentRequest, + settings: provider_utils.Settings, +) -> lib.Serializable: + # Construct the Loads + loads = [ + { + "Company": { + "Name": payload.shipper.company_name, + "Address": { + "Address1": payload.shipper.address_line1, + "Address2": payload.shipper.address_line2, + "PostalCode": payload.shipper.postal_code, + "City": payload.shipper.city, + "ProvinceCode": payload.shipper.state_code + }, + "EmergencyContact": { + "FaxNumber": "", + "CellPhoneNumber": "", + "PhoneNumber": payload.shipper.phone_number, + "PhoneNumberExtension": "", + "ContactName": payload.shipper.person_name, + "Email": payload.shipper.email + }, + "IsInvoicee": False + }, + "ExpectedArrivalTimeSlot": { + }, + "Commodities": [] + } + for parcel in payload.parcels + ] + + # Construct the Unloads + unloads = [ + { + "Number": 1, + "Company": { + "Name": payload.recipient.company_name, + "Address": { + "Address1": payload.recipient.address_line1, + "Address2": payload.recipient.address_line2, + "PostalCode": payload.recipient.postal_code, + "City": payload.recipient.city, + "ProvinceCode": payload.recipient.state_code + }, + "EmergencyContact": { + "FaxNumber": "", + "CellPhoneNumber": "", + "PhoneNumber": payload.recipient.phone_number, + "PhoneNumberExtension": "", + "ContactName": payload.recipient.person_name, + "Email": payload.recipient.email + }, + "IsInvoicee": False + }, + "ExpectedArrivalTimeSlot": { + + }, + "Commodities": [{"Code": item.title} for item in payload.parcels[0].items], + "SpecialInstructions": "", + "FloorPallets": {}, + "Freight": [ + { + "Description": freight.description, + "ClassCode": "", + "Weight": { + "Quantity": freight.weight, + "Unit": "Pound" if freight.weight_unit == "LB" else "Kilogram" + }, + "Unit": freight.packaging_type, + "Quantity": 1, + "PurchaseOrderNumbers": [] + } + for freight in payload.parcels + ] + } + ] + + # Construct the LoadTender payload + load_tender_payload = { + "ServiceLevel": payload.service, + "Stops": { + "Loads": loads, + "Unloads": unloads + }, + "Notes": "", + "ShipmentIdentifier": { + "Type": "ProBill", + "Number": payload.reference + }, + "References": [ + { + "Type": "ProBill", + "Value": payload.reference + } + ], + "ThirdPartyInvoicee": { + # "Name": payload.billing_address.company_name, + # "Address": { + # "Address1": payload.billing_address.address_line1, + # "Address2": payload.billing_address.address_line2, + # "PostalCode": payload.billing_address.postal_code, + # "City": payload.billing_address.city, + # "ProvinceCode": payload.billing_address.state_code + # } + } + , "EmergencyContact": { + }, + "IsInvoicee": True + + } + + return lib.Serializable(load_tender_payload) diff --git a/modules/connectors/morneau/karrio/providers/morneau/tracking.py b/modules/connectors/morneau/karrio/providers/morneau/tracking.py new file mode 100644 index 0000000000..cd0908dabc --- /dev/null +++ b/modules/connectors/morneau/karrio/providers/morneau/tracking.py @@ -0,0 +1,73 @@ +import typing + +import karrio.core.models as models +import karrio.lib as lib +import karrio.providers.morneau.error as error +import karrio.providers.morneau.utils as provider_utils +import karrio.schemas.morneau.trackers_response as morneau_schema + + +def parse_tracking_response( + _response: lib.Deserializable[typing.List[typing.Tuple[str, dict]]], + settings: provider_utils.Settings, +) -> typing.Tuple[typing.List[models.TrackingDetails], typing.List[models.Message]]: + responses = _response.deserialize() + + messages = sum( + [ + error.parse_error_response(response, settings, tracking_number=_) + for _, response in responses + if response.get("Message", {}).get("HttpStatusCode", None) == 400 + ], + start=[], + ) + + tracking_details = [_extract_details(details, settings) for _, details in responses if + details.get("Message", {}).get("HttpStatusCode", None) != 400] + + return tracking_details, messages + + +def _extract_details( + data: dict, + settings: provider_utils.Settings, +) -> models.TrackingDetails: + # Deserialize the response into TrackersResponseType + # data.pop('tracking_number', None) + + tracking_response = morneau_schema.TrackersResponseType(**data) + order_tracking = tracking_response.OrderTracking + + if not order_tracking: + return models.TrackingDetails(carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, tracking_number="", events=[], + estimated_delivery="", delivered=False) + + events = [ + models.TrackingEvent( + date=lib.fdate(event.DateTime.split('T')[0]), + description=event.Status, + code=event.StatusCode, + time=lib.ftime(event.DateTime.split('T')[1]), + location=event.Zone, + ) + for event in order_tracking.History + ] + + return models.TrackingDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + tracking_number=order_tracking.BillId, + events=events, + estimated_delivery="not available", + delivered=order_tracking.TripCompleted, + ) + + +def tracking_request( + payload: models.TrackingRequest, + settings: provider_utils.Settings, +) -> lib.Serializable: + request = payload.tracking_numbers + + return lib.Serializable(request) diff --git a/modules/connectors/morneau/karrio/providers/morneau/units.py b/modules/connectors/morneau/karrio/providers/morneau/units.py new file mode 100644 index 0000000000..318546d99c --- /dev/null +++ b/modules/connectors/morneau/karrio/providers/morneau/units.py @@ -0,0 +1,66 @@ +import karrio.core.units as units +import karrio.lib as lib + + +class PackagingType(lib.StrEnum): + """ Carrier specific packaging type """ + PACKAGE = "Pallet" + + """ Unified Packaging type mapping """ + pallet = PACKAGE + + +class ShippingService(lib.Enum): + """ Carrier specific services """ + morneau_standard_service = "Groupe Morneau Standard Service" + + +class ShippingOption(lib.Enum): + """ Carrier specific options """ + # morneau_option = lib.OptionEnum("code") + + """ Unified Option type mapping """ + # insurance = morneau_coverage # maps unified karrio option to carrier specific + + pass + + +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) + + +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"] + + +class ServiceType(lib.Enum): + """ Carrier specific service types """ + tracking_service = ["tracking_service"] + shipping_service = ["shipping_service"] + rates_service = ["rates_service"] + + +class CommoditiesType(lib.Enum): + """ Carrier specific Commodities types """ + rendezvous = ["RENDEZVOUS"] + pcamlivr = ["PCAMLIVR"] + home = ["HOME"] diff --git a/modules/connectors/morneau/karrio/providers/morneau/utils.py b/modules/connectors/morneau/karrio/providers/morneau/utils.py new file mode 100644 index 0000000000..b276914ad1 --- /dev/null +++ b/modules/connectors/morneau/karrio/providers/morneau/utils.py @@ -0,0 +1,90 @@ +import datetime + +import jstruct +import karrio.core as core +import karrio.lib as lib +import karrio.providers.morneau.units as units + + +class Settings(core.Settings): + """Groupe Morneau connection settings.""" + + username: str + password: str + caller_id: str + cache: lib.Cache = jstruct.JStruct[lib.Cache, False, dict(default=lib.Cache())] + billed_id: int + division: str = "Morneau" + + @property + def carrier_name(self): + return "morneau" + + # Define URLs for different services + @property + def rates_server_url(self): + return "https://cotation.groupemorneau.com/api" + + @property + def tracking_url(self): + return "https://dev-shippingapi.groupemorneau.com" if self.test_mode else "https://shippingapi.groupemorneau.com" + + @property + def server_url(self): + return "https://dev-tmorposttenderapi.groupemorneau.com" if self.test_mode else "https://tmorposttenderapi.groupemorneau.com" + + @property + def rating_jwt_token(self): + return self._retrieve_jwt_token(self.rates_server_url, units.ServiceType.rates_service) + + @property + def tracking_jwt_token(self): + return self._retrieve_jwt_token(self.tracking_url, units.ServiceType.tracking_service) + + @property + def shipment_jwt_token(self): + return self._retrieve_jwt_token(self.server_url, units.ServiceType.shipping_service) + + def _retrieve_jwt_token(self, url: str, service: units.ServiceType) -> str: + """Retrieve JWT token from the given URL.""" + cache_key = "auth_token" + now = datetime.datetime.now() + + # Check if a cached token exists and is still valid + cached = self.cache.get(cache_key) or {} + if cached and cached.get('expiry') > now: + return cached.get('token') + + if service == units.ServiceType.rates_service: + + # Perform the authentication request + response = lib.request( + url=f"{url}/auth/login", + data=f"Username={self.username}&Password={self.password}", + method="POST", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + expires_in_seconds: int = 600 + + else: + # Perform the authentication request + response = lib.request( + url=f"{url}/api/auth/Token", + # this need to be wrapped in lib.json "{"Username": self.username, "Password": self.password}" + data=lib.to_json({"UserName": self.username, "Password": self.password}), + method="POST", + headers={"Content-Type": "application/json"}, + ) + expires_in_seconds: int = 3600 + + # Parse the response and extract the token and expiry time + token_data = lib.to_dict(response) + token = token_data.get("AccessToken") + + expiry_time = now + datetime.timedelta(seconds=expires_in_seconds) + + # Cache the token and its expiry time + self.cache.set(cache_key, {"token": token, "expiry": expiry_time}) + + return token diff --git a/modules/connectors/morneau/karrio/schemas/morneau/__init__.py b/modules/connectors/morneau/karrio/schemas/morneau/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/connectors/morneau/karrio/schemas/morneau/error.py b/modules/connectors/morneau/karrio/schemas/morneau/error.py new file mode 100644 index 0000000000..fdab67a4f8 --- /dev/null +++ b/modules/connectors/morneau/karrio/schemas/morneau/error.py @@ -0,0 +1,14 @@ +from attr import s +from typing import Optional, Any +from jstruct import JStruct + + +@s(auto_attribs=True) +class GenericDetailType: + QuoteNumber: Optional[str] = None + + +@s(auto_attribs=True) +class ErrorType: + GenericDetail: Optional[GenericDetailType] = JStruct[GenericDetailType] + FailedValidation: Any = None diff --git a/modules/connectors/morneau/karrio/schemas/morneau/error_cancel_shipment.py b/modules/connectors/morneau/karrio/schemas/morneau/error_cancel_shipment.py new file mode 100644 index 0000000000..961efafaa2 --- /dev/null +++ b/modules/connectors/morneau/karrio/schemas/morneau/error_cancel_shipment.py @@ -0,0 +1,7 @@ +from attr import s +from typing import Optional + + +@s(auto_attribs=True) +class ErrorCancelShipmentType: + Message: Optional[str] = None diff --git a/modules/connectors/morneau/karrio/schemas/morneau/error_shipment.py b/modules/connectors/morneau/karrio/schemas/morneau/error_shipment.py new file mode 100644 index 0000000000..d21c2e4d38 --- /dev/null +++ b/modules/connectors/morneau/karrio/schemas/morneau/error_shipment.py @@ -0,0 +1,14 @@ +from attr import s +from typing import List, Optional +from jstruct import JStruct + + +@s(auto_attribs=True) +class ModelStateType: + loadTender: List[str] = [] + + +@s(auto_attribs=True) +class ErrorShipmentType: + Message: Optional[str] = None + ModelState: Optional[ModelStateType] = JStruct[ModelStateType] diff --git a/modules/connectors/morneau/karrio/schemas/morneau/error_tracking.py b/modules/connectors/morneau/karrio/schemas/morneau/error_tracking.py new file mode 100644 index 0000000000..1e9153148a --- /dev/null +++ b/modules/connectors/morneau/karrio/schemas/morneau/error_tracking.py @@ -0,0 +1,15 @@ +from attr import s +from typing import Optional +from jstruct import JStruct + + +@s(auto_attribs=True) +class MessageType: + Code: Optional[str] = None + HttpStatusCode: Optional[int] = None + ErrorMessage: Optional[str] = None + + +@s(auto_attribs=True) +class ErrorTrackingType: + Message: Optional[MessageType] = JStruct[MessageType] diff --git a/modules/connectors/morneau/karrio/schemas/morneau/rate_request.py b/modules/connectors/morneau/karrio/schemas/morneau/rate_request.py new file mode 100644 index 0000000000..a8829e4103 --- /dev/null +++ b/modules/connectors/morneau/karrio/schemas/morneau/rate_request.py @@ -0,0 +1,30 @@ +from attr import s +from typing import Optional, List +from jstruct import JList, JStruct + + +@s(auto_attribs=True) +class DimensionType: + Piece: Optional[int] = None + Length: Optional[int] = None + Width: Optional[int] = None + Height: Optional[int] = None + + +@s(auto_attribs=True) +class QuoteType: + StartZone: Optional[str] = None + EndZone: Optional[str] = None + UserName: Optional[str] = None + NbPallet: Optional[int] = None + Weight: Optional[int] = None + WeightUnit: Optional[str] = None + Commodities: List[str] = [] + Dimensions: List[DimensionType] = JList[DimensionType] + + +@s(auto_attribs=True) +class RateRequestType: + BillToCodeId: Optional[int] = None + Division: Optional[str] = None + Quote: Optional[QuoteType] = JStruct[QuoteType] diff --git a/modules/connectors/morneau/karrio/schemas/morneau/rate_response.py b/modules/connectors/morneau/karrio/schemas/morneau/rate_response.py new file mode 100644 index 0000000000..a0c61765a9 --- /dev/null +++ b/modules/connectors/morneau/karrio/schemas/morneau/rate_response.py @@ -0,0 +1,48 @@ +from attr import s +from typing import Optional, List, Any +from jstruct import JList, JStruct + + +@s(auto_attribs=True) +class ChargeType: + Id: Optional[str] = None + Amount: Optional[float] = None + Description: Optional[str] = None + + +@s(auto_attribs=True) +class AccessorialChargesType: + Charges: List[ChargeType] = JList[ChargeType] + TotalAmount: Optional[float] = None + + +@s(auto_attribs=True) +class RateResponseType: + DetailLineId: Optional[int] = None + QuoteNumber: Optional[str] = None + ValidFrom: Optional[str] = None + ValidTo: Optional[str] = None + Charges: Optional[float] = None + XCharges: Optional[float] = None + ProtectedCharges: Optional[float] = None + Tps: Optional[float] = None + Tvq: Optional[float] = None + TotalCharges: Optional[float] = None + IsSucessfull: Optional[bool] = None + AccessorialCharges: Optional[AccessorialChargesType] = JStruct[AccessorialChargesType] + EndZone: Optional[str] = None + EndCity: Any = None + StartZone: Optional[str] = None + StartCity: Any = None + NbPallet: Optional[int] = None + NbPalletPlancher: Optional[int] = None + NbPieces: Optional[int] = None + PiecesUnit: Optional[int] = None + WeightUnit: Optional[int] = None + RawWeightUnit: Optional[int] = None + RawPiecesUnit: Optional[str] = None + Weight: Optional[float] = None + BillToCode: Optional[str] = None + UserName: Optional[str] = None + Commodities: List[str] = [] + Dimensions: List[Any] = [] diff --git a/modules/connectors/morneau/karrio/schemas/morneau/shipment_purchase_request.py b/modules/connectors/morneau/karrio/schemas/morneau/shipment_purchase_request.py new file mode 100644 index 0000000000..efa6179ced --- /dev/null +++ b/modules/connectors/morneau/karrio/schemas/morneau/shipment_purchase_request.py @@ -0,0 +1,108 @@ +from attr import s +from typing import Optional, Any, List +from jstruct import JStruct, JList + + +@s(auto_attribs=True) +class ReferenceType: + Type: Optional[str] = None + Value: Optional[int] = None + + +@s(auto_attribs=True) +class ShipmentIdentifierType: + Type: Optional[str] = None + Number: Optional[str] = None + + +@s(auto_attribs=True) +class AddressType: + Address1: Optional[str] = None + Address2: Optional[str] = None + PostalCode: Optional[str] = None + City: Optional[str] = None + ProvinceCode: Optional[str] = None + + +@s(auto_attribs=True) +class EmergencyContactType: + FaxNumber: Optional[int] = None + CellPhoneNumber: Optional[int] = None + PhoneNumber: Optional[int] = None + PhoneNumberExtension: Optional[int] = None + ContactName: Optional[str] = None + Email: Optional[str] = None + + +@s(auto_attribs=True) +class ThirdPartyInvoiceeType: + Name: Optional[str] = None + Address: Optional[AddressType] = JStruct[AddressType] + EmergencyContact: Optional[EmergencyContactType] = JStruct[EmergencyContactType] + IsInvoicee: Optional[bool] = None + + +@s(auto_attribs=True) +class ExpectedArrivalTimeSlotType: + Between: Optional[str] = None + And: Optional[str] = None + + +@s(auto_attribs=True) +class LoadType: + Company: Optional[ThirdPartyInvoiceeType] = JStruct[ThirdPartyInvoiceeType] + ExpectedArrivalTimeSlot: Optional[ExpectedArrivalTimeSlotType] = JStruct[ExpectedArrivalTimeSlotType] + Commodities: List[Any] = [] + + +@s(auto_attribs=True) +class CommodityType: + Code: Optional[str] = None + + +@s(auto_attribs=True) +class FloorPalletsType: + Quantity: Optional[int] = None + + +@s(auto_attribs=True) +class WeightType: + Quantity: Optional[int] = None + Unit: Optional[str] = None + + +@s(auto_attribs=True) +class FreightType: + Description: Optional[str] = None + ClassCode: Optional[str] = None + Weight: Optional[WeightType] = JStruct[WeightType] + Unit: Optional[str] = None + Quantity: Optional[int] = None + PurchaseOrderNumbers: List[str] = [] + + +@s(auto_attribs=True) +class UnloadType: + Number: Optional[int] = None + Company: Optional[ThirdPartyInvoiceeType] = JStruct[ThirdPartyInvoiceeType] + ExpectedArrivalTimeSlot: Optional[ExpectedArrivalTimeSlotType] = JStruct[ExpectedArrivalTimeSlotType] + Commodities: List[CommodityType] = JList[CommodityType] + SpecialInstructions: Optional[str] = None + FloorPallets: Optional[FloorPalletsType] = JStruct[FloorPalletsType] + Freight: List[FreightType] = JList[FreightType] + + +@s(auto_attribs=True) +class StopsType: + Loads: List[LoadType] = JList[LoadType] + Unloads: List[UnloadType] = JList[UnloadType] + + +@s(auto_attribs=True) +class ShipmentPurchaseRequestType: + ServiceLevel: Optional[str] = None + Stops: Optional[StopsType] = JStruct[StopsType] + Notes: Optional[str] = None + ShipmentIdentifier: Optional[ShipmentIdentifierType] = JStruct[ShipmentIdentifierType] + References: List[ReferenceType] = JList[ReferenceType] + ThirdPartyInvoicee: Optional[ThirdPartyInvoiceeType] = JStruct[ThirdPartyInvoiceeType] diff --git a/modules/connectors/morneau/karrio/schemas/morneau/shipment_purchase_response.py b/modules/connectors/morneau/karrio/schemas/morneau/shipment_purchase_response.py new file mode 100644 index 0000000000..52dd66e1f8 --- /dev/null +++ b/modules/connectors/morneau/karrio/schemas/morneau/shipment_purchase_response.py @@ -0,0 +1,24 @@ +from attr import s +from typing import Optional, Any, List +from jstruct import JList + + +@s(auto_attribs=True) +class ReferenceType: + Type: Optional[str] = None + Value: Optional[int] = None + + +@s(auto_attribs=True) +class LoadTenderConfirmationType: + FreightBillNumber: Optional[str] = None + IsAccepted: Optional[bool] = None + Status: Optional[str] = None + PurchaseOrderNumbers: List[Any] = [] + References: List[ReferenceType] = JList[ReferenceType] + + +@s(auto_attribs=True) +class ShipmentPurchaseResponseType: + ShipmentIdentifier: Optional[str] = None + LoadTenderConfirmations: List[LoadTenderConfirmationType] = JList[LoadTenderConfirmationType] diff --git a/modules/connectors/morneau/karrio/schemas/morneau/trackers_response.py b/modules/connectors/morneau/karrio/schemas/morneau/trackers_response.py new file mode 100644 index 0000000000..692dc30b0a --- /dev/null +++ b/modules/connectors/morneau/karrio/schemas/morneau/trackers_response.py @@ -0,0 +1,73 @@ +from attr import s +from typing import Optional, Any, List +from jstruct import JStruct, JList + + +@s(auto_attribs=True) +class MessageType: + Code: Optional[str] = None + Message: Optional[str] = None + HttpStatusCode: Optional[int] = None + ErrorMessage: Any = None + + +@s(auto_attribs=True) +class CallerType: + Name: Optional[str] = None + Street: Optional[str] = None + City: Optional[str] = None + Province: Optional[str] = None + PostalCode: Optional[str] = None + + +@s(auto_attribs=True) +class HistoryType: + DateTime: Optional[str] = None + Status: Optional[str] = None + Zone: Optional[str] = None + ZoneId: Optional[str] = None + StatusCode: Optional[str] = None + Ordinal: Optional[int] = None + IsTerminal: Optional[bool] = None + TerminalAddress: Optional[str] = None + StatusReasonCode: Any = None + StatusDescription: Any = None + DeliveryTerminalZone: Any = None + IsFinalDeliveryStatus: Optional[bool] = None + + +@s(auto_attribs=True) +class HistoryTerminalType: + Sequence: Optional[int] = None + Zone: Optional[str] = None + ZoneId: Optional[str] = None + TerminalAddress: Optional[str] = None + IsTermSwitch: Optional[bool] = None + + +@s(auto_attribs=True) +class VehicleCoordinatesType: + VehicleCode: Optional[str] = None + Longitude: Optional[str] = None + Latitude: Optional[str] = None + + +@s(auto_attribs=True) +class OrderTrackingType: + BillId: Optional[str] = None + CreatedBy: Optional[str] = None + CreatedOn: Optional[str] = None + Caller: Optional[CallerType] = JStruct[CallerType] + Shipper: Optional[CallerType] = JStruct[CallerType] + Receiver: Optional[CallerType] = JStruct[CallerType] + History: List[HistoryType] = JList[HistoryType] + HistoryTerminals: List[HistoryTerminalType] = JList[HistoryTerminalType] + VehicleCoordinates: Optional[VehicleCoordinatesType] = JStruct[VehicleCoordinatesType] + HasDangerousMaterials: Optional[str] = None + TripCompleted: Optional[bool] = None + + +@s(auto_attribs=True) +class TrackersResponseType: + OrderTracking: Optional[OrderTrackingType] = JStruct[OrderTrackingType] + Message: Optional[MessageType] = JStruct[MessageType] diff --git a/modules/connectors/morneau/schemas/error.json b/modules/connectors/morneau/schemas/error.json new file mode 100644 index 0000000000..12592b8cfd --- /dev/null +++ b/modules/connectors/morneau/schemas/error.json @@ -0,0 +1,6 @@ +{ + "GenericDetail": { + "QuoteNumber": "Q829239" + }, + "FailedValidation": null +} \ No newline at end of file diff --git a/modules/connectors/morneau/schemas/error_cancel_shipment.json b/modules/connectors/morneau/schemas/error_cancel_shipment.json new file mode 100644 index 0000000000..29ab7ceb4d --- /dev/null +++ b/modules/connectors/morneau/schemas/error_cancel_shipment.json @@ -0,0 +1,3 @@ +{ + "Message": "456544 was not found for 0000005461" +} \ No newline at end of file diff --git a/modules/connectors/morneau/schemas/error_shipment.json b/modules/connectors/morneau/schemas/error_shipment.json new file mode 100644 index 0000000000..8199666809 --- /dev/null +++ b/modules/connectors/morneau/schemas/error_shipment.json @@ -0,0 +1,8 @@ +{ + "Message": "The request is invalid.", + "ModelState": { + "loadTender": [ + "Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'CreateOrderAPI.Models.LoadTender.LoadTender' because the type requires a JSON object (e.g. {\"name\":\"value\"}) to deserialize correctly.\r\nTo fix this error either change the JSON to a JSON object (e.g. {\"name\":\"value\"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.\r\nPath '', line 1, position 1." + ] + } +} \ No newline at end of file diff --git a/modules/connectors/morneau/schemas/error_tracking.json b/modules/connectors/morneau/schemas/error_tracking.json new file mode 100644 index 0000000000..a1f7a7812e --- /dev/null +++ b/modules/connectors/morneau/schemas/error_tracking.json @@ -0,0 +1,7 @@ +{ + "Message": { + "Code": "OrderTracking_BadRequest", + "HttpStatusCode": 400, + "ErrorMessage": "" + } +} \ No newline at end of file diff --git a/modules/connectors/morneau/schemas/rate_request.json b/modules/connectors/morneau/schemas/rate_request.json new file mode 100644 index 0000000000..6a45d2d56f --- /dev/null +++ b/modules/connectors/morneau/schemas/rate_request.json @@ -0,0 +1,25 @@ +{ + "BillToCodeId": 18691, + "Division": "Morneau", + "Quote": { + "StartZone": "J8Z 1V8", + "EndZone": "H1L 4M3", + "UserName": "test.user", + "NbPallet": 1, + "Weight": 110, + "WeightUnit": "LB", + "Commodities": [ + "RENDEZVOUS", + "PCAMLIVR", + "HOME" + ], + "Dimensions": [ + { + "Piece": 1, + "Length": 21, + "Width": 40, + "Height": 26 + } + ] + } +} \ No newline at end of file diff --git a/modules/connectors/morneau/schemas/rate_response.json b/modules/connectors/morneau/schemas/rate_response.json new file mode 100644 index 0000000000..73c9af5c1f --- /dev/null +++ b/modules/connectors/morneau/schemas/rate_response.json @@ -0,0 +1,54 @@ +{ + "DetailLineId": 12933572, + "QuoteNumber": "Q829200", + "ValidFrom": "2023-12-11T00:00:00-05:00", + "ValidTo": "2024-01-11T00:00:00-05:00", + "Charges": 72.8, + "XCharges": 123.3, + "ProtectedCharges": 0.0, + "Tps": 9.81, + "Tvq": 19.56, + "TotalCharges": 225.47, + "IsSucessfull": true, + "AccessorialCharges": { + "Charges": [ + { + "Id": "SC", + "Amount": 23.3, + "Description": "SURC. CARB./FUEL SURC. " + }, + { + "Id": "RENDEZVOUS", + "Amount": 50.0, + "Description": "RENDEZ-VOUS / APPOINTMENT" + }, + { + "Id": "HOME", + "Amount": 50.0, + "Description": "MAISON PRIVEE / RESIDENTIAL P/U OR DELIV" + } + ], + "TotalAmount": 123.3 + }, + "EndZone": "H1L 4M3", + "EndCity": null, + "StartZone": "J8Z 1V8", + "StartCity": null, + "NbPallet": 1, + "NbPalletPlancher": 0, + "NbPieces": 0, + "PiecesUnit": 0, + "WeightUnit": 0, + "RawWeightUnit": "0", + "RawPiecesUnit": "PCS", + "Weight": 110.0, + "BillToCode": "0000005461", + "UserName": "TEST.USER", + "Commodities": [ + "DESC", + "RENDEZVOUS", + "PCAMLIVR", + "HOME" + ], + "Dimensions": [] +} \ No newline at end of file diff --git a/modules/connectors/morneau/schemas/shipment_purchase_request.json b/modules/connectors/morneau/schemas/shipment_purchase_request.json new file mode 100644 index 0000000000..fa276a6c15 --- /dev/null +++ b/modules/connectors/morneau/schemas/shipment_purchase_request.json @@ -0,0 +1,118 @@ +{ + "ServiceLevel": "Regular", + "Stops": { + "Loads": [ + { + "Company": { + "Name": "RADIANT", + "Address": { + "Address1": "3520 LAIRD RD", + "Address2": "COPK STORLOC - 7477", + "PostalCode": "L5L5Z7", + "City": "MISSISSAUGA", + "ProvinceCode": "CA_ON" + }, + "EmergencyContact": { + "FaxNumber": 0, + "CellPhoneNumber": 0, + "PhoneNumber": 0, + "PhoneNumberExtension": 0, + "ContactName": "string", + "Email": "string" + }, + "IsInvoicee": false + }, + "ExpectedArrivalTimeSlot": { + "Between": "2023-12-12T21:14:05.3502621Z", + "And": "2023-12-12T21:14:05.3502621Z" + }, + "Commodities": [] + } + ], + "Unloads": [ + { + "Number": 1, + "Company": { + "Name": "AUX GIGANTESQUES PAS", + "Address": { + "Address1": "412 RUE DU PARC", + "Address2": "", + "PostalCode": "J7R7G6", + "City": "SAINT EUSTACHE", + "ProvinceCode": "CA_QC" + }, + "EmergencyContact": { + "FaxNumber": 0, + "CellPhoneNumber": 0, + "PhoneNumber": 0, + "PhoneNumberExtension": 0, + "ContactName": "contact 1", + "Email": "string" + }, + "IsInvoicee": false + }, + "ExpectedArrivalTimeSlot": { + "Between": "2023-12-12T21:14:05.3502621Z", + "And": "2023-12-12T21:14:05.3502621Z" + }, + "Commodities": [ + { + "Code": "CHAU" + }, + { + "Code": "TAILGATE" + } + ], + "SpecialInstructions": "This is a special instruction for this unload", + "FloorPallets": { + "Quantity": 0 + }, + "Freight": [ + { + "Description": "FAK", + "ClassCode": "string", + "Weight": { + "Quantity": 160, + "Unit": "Pound" + }, + "Unit": "Pallets", + "Quantity": 1, + "PurchaseOrderNumbers": [ + "PO1" + ] + } + ] + } + ] + }, + "Notes": "this is the note", + "ShipmentIdentifier": { + "Type": "ProBill", + "Number": "TheSID01" + }, + "References": [ + { + "Type": "ProBill", + "Value": "2712138" + } + ], + "ThirdPartyInvoicee": { + "Name": "RADIANT GLOBAL LOGISTICS (CANADA) INC.", + "Address": { + "Address1": "1280 COURTNEYPARK DR E.", + "Address2": "", + "PostalCode": "L5T1N6", + "City": "MISSISSAUGA", + "ProvinceCode": "CA_ON" + }, + "EmergencyContact": { + "FaxNumber": 0, + "CellPhoneNumber": 0, + "PhoneNumber": 9056022700, + "PhoneNumberExtension": 0, + "ContactName": "SUPER", + "Email": "radiant@groupemorneau.com" + }, + "IsInvoicee": true + } +} diff --git a/modules/connectors/morneau/schemas/shipment_purchase_response.json b/modules/connectors/morneau/schemas/shipment_purchase_response.json new file mode 100644 index 0000000000..a8366306c9 --- /dev/null +++ b/modules/connectors/morneau/schemas/shipment_purchase_response.json @@ -0,0 +1,21 @@ +{ + "ShipmentIdentifier": "00108366", + "LoadTenderConfirmations": [ + { + "FreightBillNumber": "A10480018", + "IsAccepted": true, + "Status": "New", + "PurchaseOrderNumbers": [], + "References": [ + { + "Type": "Consignee", + "Value": "226675" + }, + { + "Type": "Shipper", + "Value": "284955" + } + ] + } + ] +} \ No newline at end of file diff --git a/modules/connectors/morneau/schemas/trackers_response.json b/modules/connectors/morneau/schemas/trackers_response.json new file mode 100644 index 0000000000..b094254d00 --- /dev/null +++ b/modules/connectors/morneau/schemas/trackers_response.json @@ -0,0 +1,227 @@ +{ + "OrderTracking": { + "BillId": "A9292396", + "CreatedBy": "YLEMAY", + "CreatedOn": "2023-12-21T16:48:52.7142948-05:00", + "Caller": { + "Name": "Caller company name", + "Street": "1 AVENUE DES CANADIENS DE MONTREAL", + "City": "MONTREAL", + "Province": "QC", + "PostalCode": "G1Q 1Q9" + }, + "Shipper": { + "Name": "Shipper company name", + "Street": "1 CHEMIN DE LA POINTE NOIRE", + "City": "SEPT ILES", + "Province": "QC", + "PostalCode": "G1Q 1Q9" + }, + "Receiver": { + "Name": "Receiver company name", + "Street": "1 ALLOY COURT", + "City": "NORTH YORK", + "Province": "ON", + "PostalCode": "G1Q 1Q9" + }, + "History": [ + { + "DateTime": "2021-03-09T13:11:36", + "Status": "PICK UP DISPONIBLE / AVAILABLE", + "Zone": "SEPT ILES, QC", + "ZoneId": "G1Q 1Q9", + "StatusCode": "AVAIL", + "Ordinal": 1, + "IsTerminal": false, + "TerminalAddress": "", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + }, + { + "DateTime": "2021-07-09T07:29:22", + "Status": "EN ROUTE VERS CLIENT", + "Zone": "TERMINAL SEPT ILES", + "ZoneId": "TERMSEPTIL", + "StatusCode": "ENROUTE", + "Ordinal": 2, + "IsTerminal": true, + "TerminalAddress": "1913 GAGNON,SEPT ILES,QC,G4R 1A1", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + }, + { + "DateTime": "2021-07-09T08:48:49", + "Status": "RAMASSE / PICKED UP", + "Zone": "TERMINAL SEPT ILES", + "ZoneId": "TERMSEPTIL", + "StatusCode": "PICKD", + "Ordinal": 3, + "IsTerminal": true, + "TerminalAddress": "1913 GAGNON,SEPT ILES,QC,G4R 1A1", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + }, + { + "DateTime": "2021-07-09T10:16:47", + "Status": "RAMASSE / PICKED UP", + "Zone": "SEPT ILES, QC", + "ZoneId": "G4R 5M9", + "StatusCode": "PICKD", + "Ordinal": 4, + "IsTerminal": false, + "TerminalAddress": "", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + }, + { + "DateTime": "2021-07-09T10:17:08", + "Status": "DEPART", + "Zone": "TERMINAL SEPT ILES", + "ZoneId": "TERMSEPTIL", + "StatusCode": "DEPART", + "Ordinal": 5, + "IsTerminal": true, + "TerminalAddress": "1913 GAGNON,SEPT ILES,QC,G4R 1A1", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + }, + { + "DateTime": "2021-07-09T13:31:00", + "Status": "DANS LA COUR / IN YARD", + "Zone": "TERMINAL SEPT ILES", + "ZoneId": "TERMSEPTIL", + "StatusCode": "TERMSWITCH", + "Ordinal": 6, + "IsTerminal": true, + "TerminalAddress": "1913 GAGNON,SEPT ILES,QC,G4R 1A1", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + }, + { + "DateTime": "2021-07-09T15:16:02", + "Status": "DANS LA COUR / IN YARD", + "Zone": "TERMINAL SEPT ILES", + "ZoneId": "TERMSEPTIL", + "StatusCode": "TERMSWITCH", + "Ordinal": 7, + "IsTerminal": true, + "TerminalAddress": "1913 GAGNON,SEPT ILES,QC,G4R 1A1", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + }, + { + "DateTime": "2021-08-09T08:45:36", + "Status": "DANS LA COUR / IN YARD", + "Zone": "TERMINAL SEPT ILES", + "ZoneId": "TERMSEPTIL", + "StatusCode": "TERMSWITCH", + "Ordinal": 8, + "IsTerminal": true, + "TerminalAddress": "1913 GAGNON,SEPT ILES,QC,G4R 1A1", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + }, + { + "DateTime": "2021-08-09T08:47:33", + "Status": "QUAI / DOCKED", + "Zone": "TERMINAL SEPT ILES", + "ZoneId": "TERMSEPTIL", + "StatusCode": "DOCKED", + "Ordinal": 9, + "IsTerminal": true, + "TerminalAddress": "1913 GAGNON,SEPT ILES,QC,G4R 1A1", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + }, + { + "DateTime": "2021-08-09T08:51:17", + "Status": "DEPART", + "Zone": "TERMINAL SEPT ILES", + "ZoneId": "TERMSEPTIL", + "StatusCode": "DEPART", + "Ordinal": 10, + "IsTerminal": true, + "TerminalAddress": "1913 GAGNON,SEPT ILES,QC,G4R 1A1", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + }, + { + "DateTime": "2021-08-09T13:50:19", + "Status": "DANS LA COUR / IN YARD", + "Zone": "TERMINAL MONTREAL", + "ZoneId": "TERMMTL", + "StatusCode": "TERMSWITCH", + "Ordinal": 11, + "IsTerminal": true, + "TerminalAddress": "9601 BOUL DES SCIENCES,ANJOU,QC,H1J 0A6", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + } + ], + "HistoryTerminals": [ + { + "Sequence": 2, + "Zone": "TERMSEPTIL", + "ZoneId": "TERMSEPTIL", + "TerminalAddress": "1913 GAGNON,SEPT ILES,QC,G4R 1A1", + "IsTermSwitch": false + }, + { + "Sequence": 3, + "Zone": "TERMINAL MONTREAL", + "ZoneId": "TERMMTL", + "TerminalAddress": "9601 BOUL DES SCIENCES,ANJOU,QC,H1J 0A6", + "IsTermSwitch": true + }, + { + "Sequence": 4, + "Zone": "TERMMTL", + "ZoneId": "TERMMTL", + "TerminalAddress": "9601 BOUL DES SCIENCES,ANJOU,QC,H1J 0A6", + "IsTermSwitch": false + }, + { + "Sequence": 5, + "Zone": "TERMTORONT", + "ZoneId": "TERMTORONT", + "TerminalAddress": "1115 BOULEVARD CARDIFF,MISSIISSAUGA,ON,L3S 1L8", + "IsTermSwitch": false + } + ], + "VehicleCoordinates": { + "VehicleCode": "T724", + "Longitude": "-73.556214968363449", + "Latitude": "45.624939982096357" + }, + "HasDangerousMaterials": "False", + "TripCompleted": false + }, + "Message": { + "Code": "TrackingSuccess", + "Message": "Tracage de la facture no A9292396", + "HttpStatusCode": 200, + "ErrorMessage": null + } +} diff --git a/modules/connectors/morneau/setup.py b/modules/connectors/morneau/setup.py new file mode 100644 index 0000000000..c406a54b73 --- /dev/null +++ b/modules/connectors/morneau/setup.py @@ -0,0 +1,27 @@ + +"""Warning: This setup.py is only there for git install until poetry support git subdirectory""" +from setuptools import setup, find_namespace_packages + +with open("README.md", "r") as fh: + long_description = fh.read() + +setup( + name="karrio.morneau", + version="2023.12", + description="Karrio - Groupe Morneau Shipping Extension", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/karrioapi/karrio", + author="karrio", + author_email="hello@karrio.io", + license="Apache-2.0", + packages=find_namespace_packages(exclude=["tests.*", "tests"]), + install_requires=["karrio"], + classifiers=[ + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + ], + zip_safe=False, + include_package_data=True, +) diff --git a/modules/connectors/morneau/tests/__init__.py b/modules/connectors/morneau/tests/__init__.py new file mode 100644 index 0000000000..c59be91c2b --- /dev/null +++ b/modules/connectors/morneau/tests/__init__.py @@ -0,0 +1,4 @@ + +from tests.morneau.test_rate import * +from tests.morneau.test_tracking import * +from tests.morneau.test_shipment import * \ No newline at end of file diff --git a/modules/connectors/morneau/tests/morneau/__init__.py b/modules/connectors/morneau/tests/morneau/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/connectors/morneau/tests/morneau/fixture.py b/modules/connectors/morneau/tests/morneau/fixture.py new file mode 100644 index 0000000000..699b1049c1 --- /dev/null +++ b/modules/connectors/morneau/tests/morneau/fixture.py @@ -0,0 +1,41 @@ +import datetime + +import karrio +import karrio.lib as lib +import karrio.providers.morneau.units as units + +username = "imprimerie.gauvin" +password = "test" +caller_id = "0000005991" +billed_id = 99999 +division = "Morneau" +carrier_name = "morneau" + +expiry = datetime.datetime.now() + datetime.timedelta(hours=1) + +cached_auth = { + f"{carrier_name}|{units.ServiceType.tracking_service.value}|auth_token": dict( + token="authorizationCode", + expiry=expiry.strftime("%Y-%m-%d %H:%M:%S"), + ), + f"{carrier_name}|{units.ServiceType.shipping_service.value}|auth_token": dict( + token="authorizationCode", + expiry=expiry.strftime("%Y-%m-%d %H:%M:%S"), + ), + f"{carrier_name}|{units.ServiceType.rates_service.value}|auth_token": dict( + token="authorizationCode", + expiry=expiry.strftime("%Y-%m-%d %H:%M:%S"), + ) + +} + +gateway = karrio.gateway["morneau"].create( + dict( + username=username, + password=password, + billed_id=billed_id, + caller_id=caller_id, + cache=lib.Cache(**cached_auth) + + ) +) diff --git a/modules/connectors/morneau/tests/morneau/test_rate.py b/modules/connectors/morneau/tests/morneau/test_rate.py new file mode 100644 index 0000000000..69046fe8b3 --- /dev/null +++ b/modules/connectors/morneau/tests/morneau/test_rate.py @@ -0,0 +1,162 @@ +import unittest +from unittest.mock import patch + +import karrio +import karrio.core.models as models +import karrio.lib as lib + +from .fixture import gateway + + +class TestGroupeMorneauRating(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(request.serialize(), RateRequest) + + def test_get_rate(self): + with patch("karrio.mappers.morneau.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.rates_server_url}/quotes/add" + ) + + def test_parse_rate_response(self): + with patch("karrio.mappers.morneau.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) + + +if __name__ == "__main__": + unittest.main() + +RatePayload = { + "reference": "order #1111", + "recipient": { + "company_name": "EasyPost", + "address_line1": "417 Montgomery Street", + "address_line2": "5th Floor", + "city": "San Francisco", + "state_code": "CA", + "postal_code": "H1L4M3", + "phone_number": "415-528-7555", + }, + "shipper": { + "person_name": "George Costanza", + "company_name": "Vandelay Industries", + "address_line1": "1 E 161st St.", + "city": "Bronx", + "state_code": "NY", + "postal_code": "J8Z1V8", + }, + "parcels": [{"length": 21.0, "width": 40.0, "height": 26.0, "weight": 110.0, "weight_unit": "LB", + "items": [{"title": "RENDEZVOUS"}, {"title": "PCAMLIVR"}, {"title": "HOME"}] + }], + +} + +ParsedRateResponse = [ + [ + { + "carrier_id": "morneau", + "carrier_name": "morneau", + "service": "Regular", + "total_charge": 225.47, + "transit_days": 0, + "currency": "CAD", + }, + + ], + [], +] + +RateRequest = { + "BillToCodeId": 99999, + "Division": "Morneau", + "Quote": { + "StartZone": "J8Z 1V8", + "EndZone": "H1L 4M3", + "UserName": "imprimerie.gauvin", + "NbPallet": 1, + "Weight": 110.0, + "WeightUnit": "LB", + "Commodities": [ + "RENDEZVOUS", + "PCAMLIVR", + "HOME" + ], + "Dimensions": [ + { + "Piece": 1, + "Length": 21.0, + "Width": 40.0, + "Height": 26.0 + } + ] + } +} + +RateResponse = """{ + "DetailLineId": 12941636, + "QuoteNumber": "Q830323", + "ValidFrom": "2023-12-13T00:00:00-05:00", + "ValidTo": "2024-01-13T00:00:00-05:00", + "Charges": 72.8, + "XCharges": 123.3, + "ProtectedCharges": 0.0, + "Tps": 9.81, + "Tvq": 19.56, + "TotalCharges": 225.47, + "IsSucessfull": true, + "AccessorialCharges": { + "Charges": [ + { + "Id": "SC", + "Amount": 23.3, + "Description": "SURC. CARB./FUEL SURC. " + }, + { + "Id": "RENDEZVOUS", + "Amount": 50.0, + "Description": "RENDEZ-VOUS / APPOINTMENT" + }, + { + "Id": "HOME", + "Amount": 50.0, + "Description": "MAISON PRIVEE / RESIDENTIAL P/U OR DELIV" + } + ], + "TotalAmount": 123.3 + }, + "EndZone": "H1L 4M3", + "EndCity": null, + "StartZone": "J8Z 1V8", + "StartCity": null, + "NbPallet": 1, + "NbPalletPlancher": 0, + "NbPieces": 0, + "PiecesUnit": 0, + "WeightUnit": 0, + "RawWeightUnit": "0", + "RawPiecesUnit": "PCS", + "Weight": 110.0, + "BillToCode": "0000005461", + "UserName": "IMPRIMERIE.GAUVIN", + "Commodities": [ + "DESC", + "RENDEZVOUS", + "PCAMLIVR", + "HOME" + ], + "Dimensions": [] +} +""" diff --git a/modules/connectors/morneau/tests/morneau/test_shipment.py b/modules/connectors/morneau/tests/morneau/test_shipment.py new file mode 100644 index 0000000000..9f864554d6 --- /dev/null +++ b/modules/connectors/morneau/tests/morneau/test_shipment.py @@ -0,0 +1,260 @@ +import unittest +from unittest.mock import patch + +import karrio +import karrio.core.models as models +import karrio.lib as lib + +from .fixture import gateway + + +class TestGroupeMorneauShipping(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(request.serialize(), ShipmentRequest) + + def test_create_cancel_shipment_request(self): + request = gateway.mapper.create_cancel_shipment_request( + self.ShipmentCancelRequest + ) + + self.assertEqual(request.serialize(), ShipmentCancelRequest) + + def test_create_shipment(self): + with patch("karrio.mappers.morneau.proxy.lib.request") as mock: + mock.return_value = "{}" + karrio.Shipment.create(self.ShipmentRequest).from_(gateway) + + self.assertEqual( + mock.call_args[1]["url"], + f"{gateway.settings.server_url}/LoadTender/0000005991", + ) + + def test_cancel_shipment(self): + with patch("karrio.mappers.morneau.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}/LoadTender/0000005991/794947717776/cancel", + ) + + def test_parse_shipment_response(self): + with patch("karrio.mappers.morneau.proxy.lib.request") as mock: + mock.return_value = ShipmentResponse + parsed_response = ( + karrio.Shipment.create(self.ShipmentRequest).from_(gateway).parse() + ) + self.assertListEqual(lib.to_dict(parsed_response), ParsedShipmentResponse) + + def test_parse_cancel_shipment_response(self): + with patch("karrio.mappers.morneau.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), ParsedCancelShipmentResponse + ) + + +if __name__ == "__main__": + unittest.main() + +ShipmentPayload = { + "service": "Regular", + "reference": "order #1111", + "recipient": { + "person_name": "Morris Moss", + "company_name": "Morneau", + "address_line1": "417 Montgomery Street", + "address_line2": "", + "city": "San Francisco", + "state_code": "CA", + "postal_code": "H1L 4M3", + "phone_number": "415-528-7555", + "email": "recipient@gmail.com" + }, + "shipper": { + "person_name": "George Costanza", + "company_name": "Vandelay Industries", + "address_line1": "1 E 161st St.", + "address_line2": "", + "city": "Bronx", + "state_code": "NY", + "postal_code": "J8Z 1V8", + "phone_number": "415-528-7556", + "email": "shipper@gmail.com" + }, + "parcels": [{"length": 21.0, "width": 40.0, "height": 26.0, "weight": 110.0, "weight_unit": "LB", + "packaging_type": "Pallets", "description": "FAK", + "items": [{"title": "RENDEZVOUS"}, {"title": "PCAMLIVR"}, {"title": "HOME"}] + }], + +} + +ShipmentCancelPayload = { + "shipment_identifier": "794947717776", +} + +ParsedShipmentResponse = [ + { + "carrier_id": "morneau", + "carrier_name": "morneau", + 'docs': {}, + "label_type": "PDF", + "meta": { + "is_accepted": True, + "status": "New" + }, + "tracking_number": "A10480018", + "shipment_identifier": "00108366" + }, + [], +] + +ParsedCancelShipmentResponse = [ + { + "carrier_id": "morneau", + "carrier_name": "morneau", + "operation": "Cancel Shipment", + "success": True, + }, + [], +] + +ShipmentRequest = { + "ServiceLevel": "Regular", + "Stops": { + "Loads": [ + { + "Company": { + "Name": "Vandelay Industries", + "Address": { + "Address1": "1 E 161st St.", + "Address2": "", + "City": "Bronx", + "ProvinceCode": "NY", + "PostalCode": "J8Z 1V8" + }, + "EmergencyContact": { + "FaxNumber": "", + "CellPhoneNumber": "", + "PhoneNumber": "415-528-7556", + "PhoneNumberExtension": "", + "ContactName": "George Costanza", + "Email": "shipper@gmail.com" + }, + "IsInvoicee": False + }, + "ExpectedArrivalTimeSlot": { + }, + "Commodities": [] + } + ], + "Unloads": [ + { + "Number": 1, + "Company": { + "Name": "Morneau", + "Address": { + "Address1": "417 Montgomery Street", + "Address2": "", + "PostalCode": "H1L 4M3", + "City": "San Francisco", + "ProvinceCode": "CA" + }, + "EmergencyContact": { + "FaxNumber": "", + "CellPhoneNumber": "", + "PhoneNumber": "415-528-7555", + "PhoneNumberExtension": "", + "ContactName": "Morris Moss", + "Email": "recipient@gmail.com" + + }, + "IsInvoicee": False + }, + "ExpectedArrivalTimeSlot": { + }, + "Commodities": [ + { + "Code": "RENDEZVOUS" + }, + { + "Code": "PCAMLIVR" + }, + { + "Code": "HOME" + } + ], + "SpecialInstructions": "", + "FloorPallets": {}, + + "Freight": [ + { + "Description": "FAK", + "ClassCode": "", + "Weight": { + "Quantity": 110.0, + "Unit": "Pound" + }, + "Unit": "Pallets", + "Quantity": 1, + "PurchaseOrderNumbers": [] + } + ] + } + ] + }, + "Notes": "", + "ShipmentIdentifier": { + "Type": "ProBill", + "Number": "order #1111" + }, + "References": [ + { + "Type": "ProBill", + "Value": "order #1111" + } + ], + "ThirdPartyInvoicee": {}, + "EmergencyContact": {}, + "IsInvoicee": True +} + +ShipmentCancelRequest = {'reference': '794947717776'} + +ShipmentResponse = """{ + "ShipmentIdentifier": "00108366", + "LoadTenderConfirmations": [ + { + "FreightBillNumber": "A10480018", + "IsAccepted": true, + "Status": "New", + "PurchaseOrderNumbers": [], + "References": [ + { + "Type": "Consignee", + "Value": "226675" + }, + { + "Type": "Shipper", + "Value": "284955" + } + ] + } + ] +} +""" + +ShipmentCancelResponse = """{} +""" diff --git a/modules/connectors/morneau/tests/morneau/test_tracking.py b/modules/connectors/morneau/tests/morneau/test_tracking.py new file mode 100644 index 0000000000..545796b3fd --- /dev/null +++ b/modules/connectors/morneau/tests/morneau/test_tracking.py @@ -0,0 +1,214 @@ +import unittest +from unittest.mock import patch + +import karrio +import karrio.core.models as models +import karrio.lib as lib + +from .fixture import gateway + + +class TestGroupeMorneauTracking(unittest.TestCase): + def setUp(self): + self.maxDiff = None + self.TrackingRequest = models.TrackingRequest(**TrackingPayload) + + def test_create_tracking_request(self): + request = gateway.mapper.create_tracking_request(self.TrackingRequest) + self.assertEqual(request.serialize(), TrackingRequest) + + def test_get_tracking(self): + with patch("karrio.mappers.morneau.proxy.lib.request") as mock: + mock.return_value = "{}" + karrio.Tracking.fetch(self.TrackingRequest).from_(gateway) + + self.assertEqual( + mock.call_args[1]["url"], + f"{gateway.settings.tracking_url}/api/v1/tracking/en/MORNEAU/89108749065090", + ) + + def test_parse_tracking_response(self): + with patch("karrio.mappers.morneau.proxy.lib.request") as mock: + mock.return_value = TrackingResponse + parsed_response = ( + karrio.Tracking.fetch(self.TrackingRequest).from_(gateway).parse() + ) + + self.assertListEqual( + lib.to_dict(parsed_response), ParsedTrackingResponse + ) + + def test_parse_error_response(self): + with patch("karrio.mappers.morneau.proxy.lib.request") as mock: + mock.return_value = ErrorResponse + parsed_response = ( + karrio.Tracking.fetch(self.TrackingRequest).from_(gateway).parse() + ) + + self.assertListEqual( + lib.to_dict(parsed_response), ParsedErrorResponse + ) + + +if __name__ == "__main__": + unittest.main() + +TrackingPayload = { + "tracking_numbers": ["89108749065090"], +} + +ParsedTrackingResponse = [ + [ + { + "carrier_id": "morneau", + "carrier_name": "morneau", + "tracking_number": "89108749065090", + "events": [ + { + "date": "2021-03-09", + "description": "PICK UP DISPONIBLE / AVAILABLE", + "code": "AVAIL", + "location": "SEPT ILES, QC", + "time": "13:11", + }, + { + "date": "2021-07-09", + "description": "RAMASSE / PICKED UP", + "code": "PICKD", + "location": "TERMINAL SEPT ILES", + "time": "08:48", + }, + ], + "estimated_delivery": "not available", + "delivered": False, + } + ], + [], +] + +ParsedErrorResponse = [ + [], + [ + { + "carrier_id": "morneau", + "carrier_name": "morneau", + "code": "OrderTracking_BadRequest", + "details": {"tracking_number": "89108749065090"}, + "message": "failed", + } + ], +] + +TrackingRequest = ["89108749065090"] + +TrackingResponse = """ { + "OrderTracking": { + "BillId": "89108749065090", + "CreatedBy": "YLEMAY", + "CreatedOn": "2024-01-08T12:41:34.766218-05:00", + "Caller": { + "Name": "Caller company name", + "Street": "1 AVENUE DES CANADIENS DE MONTREAL", + "City": "MONTREAL", + "Province": "QC", + "PostalCode": "G1Q 1Q9" + }, + "Shipper": { + "Name": "Shipper company name", + "Street": "1 CHEMIN DE LA POINTE NOIRE", + "City": "SEPT ILES", + "Province": "QC", + "PostalCode": "G1Q 1Q9" + }, + "Receiver": { + "Name": "Receiver company name", + "Street": "1 ALLOY COURT", + "City": "NORTH YORK", + "Province": "ON", + "PostalCode": "G1Q 1Q9" + }, + "History": [ + { + "DateTime": "2021-03-09T13:11:36", + "Status": "PICK UP DISPONIBLE / AVAILABLE", + "Zone": "SEPT ILES, QC", + "ZoneId": "G1Q 1Q9", + "StatusCode": "AVAIL", + "Ordinal": 1, + "IsTerminal": false, + "TerminalAddress": "", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + }, + { + "DateTime": "2021-07-09T08:48:49", + "Status": "RAMASSE / PICKED UP", + "Zone": "TERMINAL SEPT ILES", + "ZoneId": "TERMSEPTIL", + "StatusCode": "PICKD", + "Ordinal": 3, + "IsTerminal": true, + "TerminalAddress": "1913 GAGNON,SEPT ILES,QC,G4R 1A1", + "StatusReasonCode": null, + "StatusDescription": null, + "DeliveryTerminalZone": null, + "IsFinalDeliveryStatus": false + } + ], + "HistoryTerminals": [ + { + "Sequence": 2, + "Zone": "TERMSEPTIL", + "ZoneId": "TERMSEPTIL", + "TerminalAddress": "1913 GAGNON,SEPT ILES,QC,G4R 1A1", + "IsTermSwitch": false + }, + { + "Sequence": 3, + "Zone": "TERMINAL MONTREAL", + "ZoneId": "TERMMTL", + "TerminalAddress": "9601 BOUL DES SCIENCES,ANJOU,QC,H1J 0A6", + "IsTermSwitch": true + }, + { + "Sequence": 4, + "Zone": "TERMMTL", + "ZoneId": "TERMMTL", + "TerminalAddress": "9601 BOUL DES SCIENCES,ANJOU,QC,H1J 0A6", + "IsTermSwitch": false + }, + { + "Sequence": 5, + "Zone": "TERMTORONT", + "ZoneId": "TERMTORONT", + "TerminalAddress": "1115 BOULEVARD CARDIFF,MISSIISSAUGA,ON,L3S 1L8", + "IsTermSwitch": false + } + ], + "VehicleCoordinates": { + "VehicleCode": "T724", + "Longitude": "-73.556214968363449", + "Latitude": "45.624939982096357" + }, + "HasDangerousMaterials": "False", + "TripCompleted": false + }, + "Message": { + "Code": "TrackingSuccess", + "Message": "Tracage de la facture no A9292396", + "HttpStatusCode": 200, + "ErrorMessage": null + } + } +""" + +ErrorResponse = """{ +"Message": { +"Code": "OrderTracking_BadRequest", +"HttpStatusCode": 400, +"ErrorMessage": "failed" +} +} +""" diff --git a/modules/core/karrio/server/providers/extension/models/freightcomv2.py b/modules/core/karrio/server/providers/extension/models/freightcomv2.py new file mode 100644 index 0000000000..ef0586b098 --- /dev/null +++ b/modules/core/karrio/server/providers/extension/models/freightcomv2.py @@ -0,0 +1,20 @@ +from django.db import models +from karrio.server.providers.models.carrier import Carrier + + +class Freightcomv2Settings(Carrier): + CARRIER_NAME = 'freightcomv2' + + class Meta: + db_table = "freightcomv2-settings" + verbose_name = 'Freightcomv2 Settings' + verbose_name_plural = 'Freightcomv2 Settings' + + apiKey = models.CharField(max_length=200) + + @property + def carrier_name(self) -> str: + return self.CARRIER_NAME + + +SETTINGS = Freightcomv2Settings diff --git a/modules/core/karrio/server/providers/extension/models/morneau.py b/modules/core/karrio/server/providers/extension/models/morneau.py new file mode 100644 index 0000000000..2d1cd6b228 --- /dev/null +++ b/modules/core/karrio/server/providers/extension/models/morneau.py @@ -0,0 +1,24 @@ +from django.db import models +from karrio.server.providers.models.carrier import Carrier + + +class MorneauSettings(Carrier): + CARRIER_NAME = "morneau" + + class Meta: + db_table = "morneau-settings" + verbose_name = "Morneau Settings" + verbose_name_plural = "Morneau Settings" + + username = models.CharField(max_length=200) + password = models.CharField(max_length=200) + billed_id = models.IntegerField() + caller_id = models.CharField(max_length=200) + division = models.CharField(max_length=100, default="Morneau") + + @property + def carrier_name(self) -> str: + return self.CARRIER_NAME + + +SETTINGS = MorneauSettings diff --git a/modules/core/karrio/server/providers/migrations/0058_morneausettings.py b/modules/core/karrio/server/providers/migrations/0058_morneausettings.py new file mode 100644 index 0000000000..e45d460806 --- /dev/null +++ b/modules/core/karrio/server/providers/migrations/0058_morneausettings.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.8 on 2023-12-14 21:53 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("providers", "0057_alter_servicelevel_weight_unit_belgianpostsettings"), + ] + + operations = [ + migrations.CreateModel( + name="MorneauSettings", + fields=[ + ( + "carrier_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="providers.carrier", + ), + ), + ("username", models.CharField(max_length=200)), + ("password", models.CharField(max_length=200)), + ("billed_id", models.IntegerField(max_length=10)), + ("division", models.CharField(default="Morneau", max_length=100)), + ], + options={ + "verbose_name": "Morneau Settings", + "verbose_name_plural": "Morneau Settings", + "db_table": "morneau-settings", + }, + bases=("providers.carrier",), + ), + ] diff --git a/modules/core/karrio/server/providers/migrations/0059_alter_morneausettings_billed_id.py b/modules/core/karrio/server/providers/migrations/0059_alter_morneausettings_billed_id.py new file mode 100644 index 0000000000..047956a481 --- /dev/null +++ b/modules/core/karrio/server/providers/migrations/0059_alter_morneausettings_billed_id.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.8 on 2023-12-18 18:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("providers", "0058_morneausettings"), + ] + + operations = [ + migrations.AlterField( + model_name="morneausettings", + name="billed_id", + field=models.IntegerField(), + ), + ] diff --git a/modules/core/karrio/server/providers/migrations/0060_morneausettings_caller_id.py b/modules/core/karrio/server/providers/migrations/0060_morneausettings_caller_id.py new file mode 100644 index 0000000000..81cffabe8f --- /dev/null +++ b/modules/core/karrio/server/providers/migrations/0060_morneausettings_caller_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.8 on 2024-01-09 13:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("providers", "0059_alter_morneausettings_billed_id"), + ] + + operations = [ + migrations.AddField( + model_name="morneausettings", + name="caller_id", + field=models.IntegerField(default=1), + preserve_default=False, + ), + ] diff --git a/modules/core/karrio/server/providers/migrations/0061_remove_morneausettings_billed_id.py b/modules/core/karrio/server/providers/migrations/0061_remove_morneausettings_billed_id.py new file mode 100644 index 0000000000..1fd11da4ac --- /dev/null +++ b/modules/core/karrio/server/providers/migrations/0061_remove_morneausettings_billed_id.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.8 on 2024-01-09 13:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("providers", "0060_morneausettings_caller_id"), + ] + + operations = [ + migrations.RemoveField( + model_name="morneausettings", + name="billed_id", + ), + ] diff --git a/modules/core/karrio/server/providers/migrations/0062_morneausettings_billed_id.py b/modules/core/karrio/server/providers/migrations/0062_morneausettings_billed_id.py new file mode 100644 index 0000000000..bbf1be37b8 --- /dev/null +++ b/modules/core/karrio/server/providers/migrations/0062_morneausettings_billed_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.8 on 2024-01-09 14:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("providers", "0061_remove_morneausettings_billed_id"), + ] + + operations = [ + migrations.AddField( + model_name="morneausettings", + name="billed_id", + field=models.IntegerField(default=1), + preserve_default=False, + ), + ] diff --git a/modules/core/karrio/server/providers/migrations/0063_alter_morneausettings_caller_id.py b/modules/core/karrio/server/providers/migrations/0063_alter_morneausettings_caller_id.py new file mode 100644 index 0000000000..7d133f6f21 --- /dev/null +++ b/modules/core/karrio/server/providers/migrations/0063_alter_morneausettings_caller_id.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.8 on 2024-01-09 17:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("providers", "0062_morneausettings_billed_id"), + ] + + operations = [ + migrations.AlterField( + model_name="morneausettings", + name="caller_id", + field=models.CharField(max_length=200), + ), + ] diff --git a/modules/core/karrio/server/providers/migrations/0072_merge_20240325_1609.py b/modules/core/karrio/server/providers/migrations/0072_merge_20240325_1609.py new file mode 100644 index 0000000000..e68c0b2d2b --- /dev/null +++ b/modules/core/karrio/server/providers/migrations/0072_merge_20240325_1609.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.11 on 2024-03-25 16:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("providers", "0063_alter_morneausettings_caller_id"), + ("providers", "0071_alter_tgesettings_my_toll_token"), + ] + + operations = [] diff --git a/modules/core/karrio/server/providers/migrations/0073_merge_20240612_1858.py b/modules/core/karrio/server/providers/migrations/0073_merge_20240612_1858.py new file mode 100644 index 0000000000..ac75c016b7 --- /dev/null +++ b/modules/core/karrio/server/providers/migrations/0073_merge_20240612_1858.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.11 on 2024-06-12 18:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("providers", "0072_merge_20240325_1609"), + ("providers", "0072_rename_eshippersettings_eshipperxmlsettings_and_more"), + ] + + operations = [] diff --git a/modules/core/karrio/server/providers/migrations/0074_freightcomv2settings.py b/modules/core/karrio/server/providers/migrations/0074_freightcomv2settings.py new file mode 100644 index 0000000000..a7bc067743 --- /dev/null +++ b/modules/core/karrio/server/providers/migrations/0074_freightcomv2settings.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.11 on 2024-06-12 19:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("providers", "0073_merge_20240612_1858"), + ] + + operations = [ + migrations.CreateModel( + name="Freightcomv2Settings", + fields=[ + ( + "carrier_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="providers.carrier", + ), + ), + ("apiKey", models.CharField(max_length=200)), + ], + options={ + "verbose_name": "Freightcomv2 Settings", + "verbose_name_plural": "Freightcomv2 Settings", + "db_table": "freightcomv2-settings", + }, + bases=("providers.carrier",), + ), + ] diff --git a/modules/pricing/karrio/server/pricing/migrations/0039_alter_surcharge_carriers.py b/modules/pricing/karrio/server/pricing/migrations/0039_alter_surcharge_carriers.py new file mode 100644 index 0000000000..f28acc2d14 --- /dev/null +++ b/modules/pricing/karrio/server/pricing/migrations/0039_alter_surcharge_carriers.py @@ -0,0 +1,59 @@ +# Generated by Django 4.2.8 on 2023-12-14 21:53 + +from django.db import migrations +import karrio.server.core.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("pricing", "0038_alter_surcharge_carriers_alter_surcharge_services"), + ] + + operations = [ + migrations.AlterField( + model_name="surcharge", + name="carriers", + field=karrio.server.core.fields.MultiChoiceField( + blank=True, + choices=[ + ("amazon_shipping", "amazon_shipping"), + ("aramex", "aramex"), + ("asendia_us", "asendia_us"), + ("australiapost", "australiapost"), + ("boxknight", "boxknight"), + ("bpost", "bpost"), + ("canadapost", "canadapost"), + ("canpar", "canpar"), + ("chronopost", "chronopost"), + ("colissimo", "colissimo"), + ("dhl_express", "dhl_express"), + ("dhl_poland", "dhl_poland"), + ("dhl_universal", "dhl_universal"), + ("dicom", "dicom"), + ("dpd", "dpd"), + ("dpdhl", "dpdhl"), + ("easypost", "easypost"), + ("eshipper", "eshipper"), + ("fedex", "fedex"), + ("freightcom", "freightcom"), + ("generic", "generic"), + ("geodis", "geodis"), + ("laposte", "laposte"), + ("locate2u", "locate2u"), + ("morneau", "morneau"), + ("nationex", "nationex"), + ("purolator", "purolator"), + ("roadie", "roadie"), + ("royalmail", "royalmail"), + ("sendle", "sendle"), + ("tnt", "tnt"), + ("ups", "ups"), + ("usps", "usps"), + ("usps_international", "usps_international"), + ("zoom2u", "zoom2u"), + ], + help_text="\n The list of carriers you want to apply the surcharge to.\n
\n Note that by default, the surcharge is applied to all carriers\n ", + null=True, + ), + ), + ] diff --git a/modules/pricing/karrio/server/pricing/migrations/0050_merge_20240325_1609.py b/modules/pricing/karrio/server/pricing/migrations/0050_merge_20240325_1609.py new file mode 100644 index 0000000000..02b945c793 --- /dev/null +++ b/modules/pricing/karrio/server/pricing/migrations/0050_merge_20240325_1609.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.11 on 2024-03-25 16:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("pricing", "0039_alter_surcharge_carriers"), + ("pricing", "0049_alter_surcharge_carriers_alter_surcharge_services"), + ] + + operations = [] diff --git a/modules/pricing/karrio/server/pricing/migrations/0051_alter_surcharge_carriers.py b/modules/pricing/karrio/server/pricing/migrations/0051_alter_surcharge_carriers.py new file mode 100644 index 0000000000..ed02db8c1f --- /dev/null +++ b/modules/pricing/karrio/server/pricing/migrations/0051_alter_surcharge_carriers.py @@ -0,0 +1,65 @@ +# Generated by Django 4.2.11 on 2024-03-25 16:40 + +from django.db import migrations +import karrio.server.core.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("pricing", "0050_merge_20240325_1609"), + ] + + operations = [ + migrations.AlterField( + model_name="surcharge", + name="carriers", + field=karrio.server.core.fields.MultiChoiceField( + blank=True, + choices=[ + ("allied_express", "allied_express"), + ("allied_express_local", "allied_express_local"), + ("amazon_shipping", "amazon_shipping"), + ("aramex", "aramex"), + ("asendia_us", "asendia_us"), + ("australiapost", "australiapost"), + ("boxknight", "boxknight"), + ("bpost", "bpost"), + ("canadapost", "canadapost"), + ("canpar", "canpar"), + ("chronopost", "chronopost"), + ("colissimo", "colissimo"), + ("dhl_express", "dhl_express"), + ("dhl_parcel_de", "dhl_parcel_de"), + ("dhl_poland", "dhl_poland"), + ("dhl_universal", "dhl_universal"), + ("dicom", "dicom"), + ("dpd", "dpd"), + ("dpdhl", "dpdhl"), + ("easypost", "easypost"), + ("eshipper", "eshipper"), + ("fedex", "fedex"), + ("fedex_ws", "fedex_ws"), + ("freightcom", "freightcom"), + ("generic", "generic"), + ("geodis", "geodis"), + ("laposte", "laposte"), + ("locate2u", "locate2u"), + ("morneau", "morneau"), + ("nationex", "nationex"), + ("purolator", "purolator"), + ("roadie", "roadie"), + ("royalmail", "royalmail"), + ("sendle", "sendle"), + ("tge", "tge"), + ("tnt", "tnt"), + ("ups", "ups"), + ("usps", "usps"), + ("usps_international", "usps_international"), + ("zoom2u", "zoom2u"), + ], + help_text="\n The list of carriers you want to apply the surcharge to.\n
\n Note that by default, the surcharge is applied to all carriers\n ", + null=True, + ), + ), + ] diff --git a/modules/pricing/karrio/server/pricing/migrations/0052_merge_20240612_1858.py b/modules/pricing/karrio/server/pricing/migrations/0052_merge_20240612_1858.py new file mode 100644 index 0000000000..2490578178 --- /dev/null +++ b/modules/pricing/karrio/server/pricing/migrations/0052_merge_20240612_1858.py @@ -0,0 +1,13 @@ +# Generated by Django 4.2.11 on 2024-06-12 18:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("pricing", "0050_alter_surcharge_carriers"), + ("pricing", "0051_alter_surcharge_carriers"), + ] + + operations = [] diff --git a/modules/pricing/karrio/server/pricing/migrations/0053_alter_surcharge_carriers.py b/modules/pricing/karrio/server/pricing/migrations/0053_alter_surcharge_carriers.py new file mode 100644 index 0000000000..5a01255827 --- /dev/null +++ b/modules/pricing/karrio/server/pricing/migrations/0053_alter_surcharge_carriers.py @@ -0,0 +1,65 @@ +# Generated by Django 4.2.11 on 2024-06-12 19:20 + +from django.db import migrations +import karrio.server.core.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("pricing", "0052_merge_20240612_1858"), + ] + + operations = [ + migrations.AlterField( + model_name="surcharge", + name="carriers", + field=karrio.server.core.fields.MultiChoiceField( + blank=True, + choices=[ + ("allied_express", "allied_express"), + ("allied_express_local", "allied_express_local"), + ("amazon_shipping", "amazon_shipping"), + ("aramex", "aramex"), + ("asendia_us", "asendia_us"), + ("australiapost", "australiapost"), + ("boxknight", "boxknight"), + ("bpost", "bpost"), + ("canadapost", "canadapost"), + ("canpar", "canpar"), + ("chronopost", "chronopost"), + ("colissimo", "colissimo"), + ("dhl_express", "dhl_express"), + ("dhl_parcel_de", "dhl_parcel_de"), + ("dhl_poland", "dhl_poland"), + ("dhl_universal", "dhl_universal"), + ("dicom", "dicom"), + ("dpd", "dpd"), + ("dpdhl", "dpdhl"), + ("easypost", "easypost"), + ("eshipper_xml", "eshipper_xml"), + ("fedex", "fedex"), + ("fedex_ws", "fedex_ws"), + ("freightcom", "freightcom"), + ("generic", "generic"), + ("geodis", "geodis"), + ("laposte", "laposte"), + ("locate2u", "locate2u"), + ("morneau", "morneau"), + ("nationex", "nationex"), + ("purolator", "purolator"), + ("roadie", "roadie"), + ("royalmail", "royalmail"), + ("sendle", "sendle"), + ("tge", "tge"), + ("tnt", "tnt"), + ("ups", "ups"), + ("usps", "usps"), + ("usps_international", "usps_international"), + ("zoom2u", "zoom2u"), + ], + help_text="\n The list of carriers you want to apply the surcharge to.\n
\n Note that by default, the surcharge is applied to all carriers\n ", + null=True, + ), + ), + ] diff --git a/modules/pricing/karrio/server/pricing/migrations/0054_alter_surcharge_carriers.py b/modules/pricing/karrio/server/pricing/migrations/0054_alter_surcharge_carriers.py new file mode 100644 index 0000000000..2c9fa39ea1 --- /dev/null +++ b/modules/pricing/karrio/server/pricing/migrations/0054_alter_surcharge_carriers.py @@ -0,0 +1,66 @@ +# Generated by Django 4.2.11 on 2024-06-12 19:24 + +from django.db import migrations +import karrio.server.core.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("pricing", "0053_alter_surcharge_carriers"), + ] + + operations = [ + migrations.AlterField( + model_name="surcharge", + name="carriers", + field=karrio.server.core.fields.MultiChoiceField( + blank=True, + choices=[ + ("allied_express", "allied_express"), + ("allied_express_local", "allied_express_local"), + ("amazon_shipping", "amazon_shipping"), + ("aramex", "aramex"), + ("asendia_us", "asendia_us"), + ("australiapost", "australiapost"), + ("boxknight", "boxknight"), + ("bpost", "bpost"), + ("canadapost", "canadapost"), + ("canpar", "canpar"), + ("chronopost", "chronopost"), + ("colissimo", "colissimo"), + ("dhl_express", "dhl_express"), + ("dhl_parcel_de", "dhl_parcel_de"), + ("dhl_poland", "dhl_poland"), + ("dhl_universal", "dhl_universal"), + ("dicom", "dicom"), + ("dpd", "dpd"), + ("dpdhl", "dpdhl"), + ("easypost", "easypost"), + ("eshipper_xml", "eshipper_xml"), + ("fedex", "fedex"), + ("fedex_ws", "fedex_ws"), + ("freightcom", "freightcom"), + ("freightcomv2", "freightcomv2"), + ("generic", "generic"), + ("geodis", "geodis"), + ("laposte", "laposte"), + ("locate2u", "locate2u"), + ("morneau", "morneau"), + ("nationex", "nationex"), + ("purolator", "purolator"), + ("roadie", "roadie"), + ("royalmail", "royalmail"), + ("sendle", "sendle"), + ("tge", "tge"), + ("tnt", "tnt"), + ("ups", "ups"), + ("usps", "usps"), + ("usps_international", "usps_international"), + ("zoom2u", "zoom2u"), + ], + help_text="\n The list of carriers you want to apply the surcharge to.\n
\n Note that by default, the surcharge is applied to all carriers\n ", + null=True, + ), + ), + ] diff --git a/requirements.server.dev.txt b/requirements.server.dev.txt index 41001e8a4f..f16e7d89bc 100644 --- a/requirements.server.dev.txt +++ b/requirements.server.dev.txt @@ -43,6 +43,8 @@ Django==4.2.11 -e ./modules/connectors/freightcom -e ./modules/connectors/locate2u -e ./modules/connectors/zoom2u +-e ./modules/connectors/morneau +-e ./modules/connectors/freightcomv2 # karrio server modules