diff --git a/README.md b/README.md index ba8cec1edb..d807c2c20c 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,23 @@ ___ #### Fluent API +##### Address + +- Validation + +```python +import purplship +from purplship.core.models import AddressValidationRequest + +carrier = purplship.gateway['carrier'].create(...) + +request = purplship.Address.validate( + AddressValidationRequest(...) +) + +rates = request.from_(carrier).parse() +``` + ##### Pickup - Booking @@ -251,7 +268,7 @@ from purplship.core.models import PickupRequest carrier = purplship.gateway['carrier'].create(...) -request = purplship.Pickup.book( +request = purplship.Pickup.schedule( PickupRequest(...) ) @@ -277,12 +294,12 @@ rates = request.from_(carrier).parse() ```python import purplship -from purplship.core.models import PickupCancellationRequest +from purplship.core.models import PickupCancelRequest carrier = purplship.gateway['carrier'].create(...) request = purplship.Pickup.cancel( - PickupCancellationRequest(...) + PickupCancelRequest(...) ) rates = request.from_(carrier).parse() @@ -322,6 +339,21 @@ request = purplship.Shipment.create( rates = request.with_(carrier).parse() ``` +- Void + +```python +import purplship +from purplship.core.models import ShipmentCancelRequest + +carrier = purplship.gateway['carrier'].create(...) + +request = purplship.Shipment.cancel( + ShipmentCancelRequest(...) +) + +rates = request.from_(carrier).parse() +``` + ##### Tracking - Fetch @@ -353,6 +385,13 @@ rates = request.from_(carrier).parse() | `password` | `str` | **required** | `custumer_number` | `str` | +- Canpar + + | Name | Type | Description + | --- | --- | --- | + | `user_id` | `str` | **required** + | `password` | `str` | **required** + - DHL Express @@ -570,7 +609,7 @@ rates = request.from_(carrier).parse() | `contact` | [Address](#Address) | -- PickupCancellationRequest +- PickupCancelRequest | Name | Type | Description | --- | --- | --- | | `confirmation_number` | `str` | @@ -649,6 +688,12 @@ rates = request.from_(carrier).parse() | `reference` | `str` | +- ShipmentCancelRequest + | Name | Type | Description + | --- | --- | --- | + | `shipment_identifier` | `str` | + + - ShipmentDetails | Name | Type | Description | --- | --- | --- | @@ -682,6 +727,7 @@ rates = request.from_(carrier).parse() | `carrier_id` | `str` | | `tracking_number` | `str` | | `events` | List[[TrackingEvent](#TrackingEvent)] | + | `delivered` | `bool` | - TrackingEvent @@ -700,7 +746,7 @@ rates = request.from_(carrier).parse() | --- | --- | --- | | `tracking_numbers` | List[str] | | `language_code` | `str` | - | `level_of_details` | `str` | + | `level_of_details` | `str` | @@ -881,6 +927,23 @@ rates = request.from_(carrier).parse() | `canadapost_tracked_packet_international` | INT.TP +- Canpar + Code | Identifier + --- | --- + | `canpar_ground` | 1 + | `canpar_usa` | 2 + | `canpar_select_letter` | 3 + | `canpar_select_pak` | 4 + | `canpar_select` | 5 + | `canpar_overnight_letter` | C + | `canpar_overnight_pak` | D + | `canpar_overnight` | E + | `canpar_usa_letter` | F + | `canpar_usa_pak` | G + | `canpar_select_usa` | H + | `canpar_international` | I + + - DHL Express Code | Identifier --- | --- @@ -1099,6 +1162,19 @@ rates = request.from_(carrier).parse() | `canadapost_abandon` | ABAN +- Canpar + Code | Identifier + --- | --- + | `canpar_cash_on_delivery` | N + | `canpar_dangerous_goods` | dg + | `canpar_extra_care` | xc + | `canpar_ten_am` | A + | `canpar_noon` | B + | `canpar_no_signature_required` | 2 + | `canpar_not_no_signature_required` | 0 + | `canpar_saturday` | S + + - DHL Express Code | Identifier --- | --- @@ -1449,4 +1525,5 @@ rates = request.from_(carrier).parse() | `sdl_shipment_indicator` | SDLShipmentIndicator | `epra_indicator` | EPRAIndicator + diff --git a/cli.py b/cli.py index 129af2cf10..13bca9d7e7 100644 --- a/cli.py +++ b/cli.py @@ -76,6 +76,12 @@ def import_pkg(pkg: str): 'options': "OptionCode", 'packagePresets': "PackagePresets" }, + 'canpar': { + 'label': "Canpar", + 'package': import_pkg('purplship.providers.canpar.units'), + 'services': "Service", + 'options': "Option" + }, 'dhl_express': { 'label': "DHL Express", 'package': import_pkg('purplship.providers.dhl_express.units'), diff --git a/extensions/canadapost/purplship/mappers/canadapost/mapper.py b/extensions/canadapost/purplship/mappers/canadapost/mapper.py index 395e191f9c..d67d821827 100644 --- a/extensions/canadapost/purplship/mappers/canadapost/mapper.py +++ b/extensions/canadapost/purplship/mappers/canadapost/mapper.py @@ -1,9 +1,5 @@ from typing import List, Tuple from pycanadapost.rating import mailing_scenario -from pycanadapost.pickuprequest import ( - PickupRequestDetailsType, - PickupRequestResponseDetailsType, -) from purplship.core.utils.pipeline import Pipeline from purplship.core.utils.serializable import Serializable, Deserializable from purplship.api.mapper import Mapper as BaseMapper @@ -18,8 +14,9 @@ PickupRequest, PickupDetails, PickupUpdateRequest, - PickupCancellationRequest, + PickupCancelRequest, ConfirmationDetails, + ShipmentCancelRequest, ) from purplship.providers.canadapost import ( mailing_scenario_request, @@ -33,6 +30,8 @@ update_pickup_request, parse_pickup_response, parse_cancel_pickup_response, + parse_void_shipment_response, + void_shipment_request, ) from purplship.mappers.canadapost.settings import Settings @@ -59,19 +58,22 @@ def create_shipment_request( def create_pickup_request( self, payload: PickupRequest - ) -> Serializable[PickupRequestDetailsType]: + ) -> Serializable[Pipeline]: return create_pickup_request(payload, self.settings) - def create_modify_pickup_request( + def create_pickup_update_request( self, payload: PickupUpdateRequest - ) -> Serializable[PickupRequestResponseDetailsType]: + ) -> Serializable[Pipeline]: return update_pickup_request(payload, self.settings) def create_cancel_pickup_request( - self, payload: PickupCancellationRequest + self, payload: PickupCancelRequest ) -> Serializable[str]: return cancel_pickup_request(payload, self.settings) + def create_cancel_shipment_request(self, payload: ShipmentCancelRequest) -> Serializable[str]: + return void_shipment_request(payload, self.settings) + """Response Parsers""" def parse_rate_response( @@ -94,7 +96,7 @@ def parse_pickup_response( ) -> Tuple[PickupDetails, List[Message]]: return parse_pickup_response(response.deserialize(), self.settings) - def parse_modify_pickup_response( + def parse_pickup_update_response( self, response: Deserializable[str] ) -> Tuple[PickupDetails, List[Message]]: return parse_pickup_response(response.deserialize(), self.settings) @@ -103,3 +105,8 @@ def parse_cancel_pickup_response( self, response: Deserializable[str] ) -> Tuple[ConfirmationDetails, List[Message]]: return parse_cancel_pickup_response(response.deserialize(), self.settings) + + def parse_cancel_shipment_response( + self, response: Deserializable + ) -> Tuple[ConfirmationDetails, List[Message]]: + return parse_void_shipment_response(response.deserialize(), self.settings) diff --git a/extensions/canadapost/purplship/mappers/canadapost/proxy.py b/extensions/canadapost/purplship/mappers/canadapost/proxy.py index 5e291d0552..61ba675167 100644 --- a/extensions/canadapost/purplship/mappers/canadapost/proxy.py +++ b/extensions/canadapost/purplship/mappers/canadapost/proxy.py @@ -1,5 +1,7 @@ import base64 from typing import List +from pycanadapost.rating import mailing_scenario +from purplship.api.proxy import Proxy as BaseProxy from purplship.core.errors import PurplShipError from purplship.core.utils.serializable import Serializable, Deserializable from purplship.core.utils.pipeline import Pipeline, Job @@ -9,9 +11,8 @@ exec_parrallel, bundle_xml, ) +from purplship.providers.canadapost import process_error from purplship.mappers.canadapost.settings import Settings -from purplship.api.proxy import Proxy as BaseProxy -from pycanadapost.rating import mailing_scenario class Proxy(BaseProxy): @@ -25,7 +26,7 @@ def get_rates(self, request: Serializable[mailing_scenario]) -> Deserializable[s "Content-Type": "application/vnd.cpc.ship.rate-v4+xml", "Accept": "application/vnd.cpc.ship.rate-v4+xml", "Authorization": f"Basic {self.settings.authorization}", - "Accept-language": "en-CA", + "Accept-language": f"{self.settings.language}-CA", }, method="POST", ) @@ -42,7 +43,7 @@ def track(tracking_pin: str) -> str: headers={ "Accept": "application/vnd.cpc.track+xml", "Authorization": f"Basic {self.settings.authorization}", - "Accept-language": "en-CA", + "Accept-language": f"{self.settings.language}-CA", }, method="GET", ) @@ -60,7 +61,7 @@ def _contract_shipment(job: Job): "Content-Type": "application/vnd.cpc.shipment-v8+xml", "Accept": "application/vnd.cpc.shipment-v8+xml", "Authorization": f"Basic {self.settings.authorization}", - "Accept-language": "en-CA", + "Accept-language": f"{self.settings.language}-CA", }, method="POST", ) @@ -73,7 +74,7 @@ def _non_contract_shipment(job: Job): "Accept": "application/vnd.cpc.ncshipment-v4+xml", "Content-Type": "application/vnd.cpc.ncshipment-v4+xml", "Authorization": f"Basic {self.settings.authorization}", - "Accept-language": "en-CA", + "Accept-language": f"{self.settings.language}-CA", }, method="POST", ) @@ -109,14 +110,30 @@ def process(job: Job): return Deserializable(bundle_xml(response), to_xml) - def request_pickup(self, request: Serializable[Pipeline]) -> Deserializable[str]: + def cancel_shipment(self, request: Serializable) -> Deserializable: + shipment_id = request.serialize() + response = http( + url=f"{self.settings.server_url}/rs/{self.settings.customer_number}/{self.settings.customer_number}/shipment/{shipment_id}", + headers={ + "Content-Type": "application/vnd.cpc.shipment-v8+xml", + "Accept": "application/vnd.cpc.shipment-v8+xml", + "Authorization": f"Basic {self.settings.authorization}", + "Accept-language": f"{self.settings.language}-CA", + }, + method="DELETE", + on_error=process_error, + ) + + return Deserializable(response or "", to_xml) + + def schedule_pickup(self, request: Serializable[Pipeline]) -> Deserializable[str]: def _availability(job: Job) -> str: return http( url=f"{self.settings.server_url}/ad/pickup/pickupavailability/{job.data}", headers={ "Accept": "application/vnd.cpc.pickup+xml", "Authorization": f"Basic {self.settings.authorization}", - "Accept-language": "en-CA", + "Accept-language": f"{self.settings.language}-CA", }, method="GET", ) @@ -129,7 +146,7 @@ def _create_pickup(job: Job) -> str: "Accept": "application/vnd.cpc.pickuprequest+xml", "Content-Type": "application/vnd.cpc.pickuprequest+xml", "Authorization": f"Basic {self.settings.authorization}", - "Accept-language": "en-CA", + "Accept-language": f"{self.settings.language}-CA", }, method="POST", ) @@ -153,18 +170,47 @@ def process(job: Job): return Deserializable(bundle_xml(response), to_xml) def modify_pickup(self, request: Serializable[dict]) -> Deserializable[str]: - payload = request.serialize() - response = http( - url=f"{self.settings.server_url}/enab/{self.settings.customer_number}/pickuprequest/{payload['pickuprequest']}", - data=bytearray(payload["data"], "utf-8"), - headers={ - "Accept": "application/vnd.cpc.pickuprequest+xml", - "Authorization": f"Basic {self.settings.authorization}", - "Accept-language": "en-CA", - }, - method="PUT", - ) - return Deserializable(response, to_xml) + def _get_pickup(job: Job) -> str: + return http( + url=f"{self.settings.server_url}{job.data.serialize()}", + headers={ + "Accept": "application/vnd.cpc.pickup+xml", + "Authorization": f"Basic {self.settings.authorization}", + "Accept-language": f"{self.settings.language}-CA", + }, + method="GET", + ) + + def _update_pickup(job: Job) -> str: + payload = job.data.serialize() + return http( + url=f"{self.settings.server_url}/enab/{self.settings.customer_number}/pickuprequest/{payload['pickuprequest']}", + data=bytearray(payload["data"], "utf-8"), + headers={ + "Accept": "application/vnd.cpc.pickuprequest+xml", + "Authorization": f"Basic {self.settings.authorization}", + "Accept-language": f"{self.settings.language}-CA", + }, + method="PUT", + ) + + def process(job: Job): + if job.data is None: + return job.fallback + + subprocess = { + "update_pickup": _update_pickup, + "get_pickup": _get_pickup, + } + if job.id not in subprocess: + raise PurplShipError(f"Unknown pickup request job id: {job.id}") + + return subprocess[job.id](job) + + pipeline: Pipeline = request.serialize() + response = pipeline.apply(process) + + return Deserializable(bundle_xml(response), to_xml) def cancel_pickup(self, request: Serializable[str]) -> Deserializable[str]: pickuprequest = request.serialize() @@ -173,7 +219,7 @@ def cancel_pickup(self, request: Serializable[str]) -> Deserializable[str]: headers={ "Accept": "application/vnd.cpc.pickuprequest+xml", "Authorization": f"Basic {self.settings.authorization}", - "Accept-language": "en-CA", + "Accept-language": f"{self.settings.language}-CA", }, method="DELETE", ) diff --git a/extensions/canadapost/purplship/mappers/canadapost/settings.py b/extensions/canadapost/purplship/mappers/canadapost/settings.py index f9325e09bb..92fd8de14c 100644 --- a/extensions/canadapost/purplship/mappers/canadapost/settings.py +++ b/extensions/canadapost/purplship/mappers/canadapost/settings.py @@ -12,6 +12,7 @@ class Settings(BaseSettings): password: str customer_number: str contract_id: str = None + language: str = "en" id: str = None test: bool = False carrier_id: str = "canadapost" diff --git a/extensions/canadapost/purplship/providers/canadapost/__init__.py b/extensions/canadapost/purplship/providers/canadapost/__init__.py index f87ff29f7c..8fac7ac436 100644 --- a/extensions/canadapost/purplship/providers/canadapost/__init__.py +++ b/extensions/canadapost/purplship/providers/canadapost/__init__.py @@ -1,7 +1,10 @@ from purplship.providers.canadapost.utils import Settings +from purplship.providers.canadapost.error import process_error from purplship.providers.canadapost.shipment import ( shipment_request, parse_shipment_response, + void_shipment_request, + parse_void_shipment_response, ) from purplship.providers.canadapost.rating import ( mailing_scenario_request, diff --git a/extensions/canadapost/purplship/providers/canadapost/error.py b/extensions/canadapost/purplship/providers/canadapost/error.py index dc2b76ea8d..34093c219d 100644 --- a/extensions/canadapost/purplship/providers/canadapost/error.py +++ b/extensions/canadapost/purplship/providers/canadapost/error.py @@ -1,5 +1,6 @@ -from typing import List, Callable +from typing import List, Callable, cast, Any from functools import reduce +from urllib.error import HTTPError from purplship.core.utils.xml import Element from purplship.providers.canadapost import Settings from purplship.core.models import Message @@ -27,3 +28,13 @@ def extract(errors: List[Message], message_node: Element) -> List[Message]: ] return extract + + +def process_error(error: HTTPError) -> str: + return f""" + + {error.code} + {cast(Any, error).msg} + + + """ diff --git a/extensions/canadapost/purplship/providers/canadapost/pickup/__init__.py b/extensions/canadapost/purplship/providers/canadapost/pickup/__init__.py index e9a380bcd9..e98be1d62c 100644 --- a/extensions/canadapost/purplship/providers/canadapost/pickup/__init__.py +++ b/extensions/canadapost/purplship/providers/canadapost/pickup/__init__.py @@ -1,11 +1,12 @@ from functools import partial from typing import cast from pycanadapost.pickup import pickup_availability -from purplship.core.utils import Job, Pipeline, to_xml, Serializable +from purplship.core.utils import Job, Pipeline, to_xml, Serializable, bundle_xml from purplship.core.utils.soap import build from purplship.core.models import PickupRequest, PickupUpdateRequest from purplship.providers.canadapost.utils import Settings +from purplship.providers.canadapost.error import parse_error_response from purplship.providers.canadapost.pickup.pickup_request import ( parse_pickup_response, pickup_request, @@ -20,9 +21,7 @@ def create_pickup_request( payload: PickupRequest, settings: Settings ) -> Serializable[Pipeline]: request: Pipeline = Pipeline( - get_availability=lambda *_: partial( - _get_pickup_availability, payload=payload - )(), + get_availability=lambda *_: _get_pickup_availability(payload), create_pickup=partial(_create_pickup, payload=payload, settings=settings), ) return Serializable(request) @@ -30,12 +29,12 @@ def create_pickup_request( def update_pickup_request( payload: PickupUpdateRequest, settings: Settings -) -> Serializable[dict]: - request = dict( - confirmation_number=payload.confirmation_number, - data=pickup_request(cast(PickupRequest, payload), settings, update=True), +) -> Serializable[Pipeline]: + request: Pipeline = Pipeline( + update_pickup=lambda *_: _update_pickup(payload, settings), + get_pickup=partial(_get_pickup, payload=payload, settings=settings), ) - return Serializable(request, _update_request_serializer) + return Serializable(request) def _get_pickup_availability(payload: PickupRequest): @@ -46,12 +45,25 @@ def _create_pickup( availability_response: str, payload: PickupRequest, settings: Settings ): availability = build(pickup_availability, to_xml(availability_response)) + data = pickup_request(payload, settings) if availability.on_demand_tour else None - return Job( - id="create_pickup", - data=pickup_request(payload, settings) if availability.on_demand_tour else None, - fallback="", - ) + return Job(id="create_pickup", data=data, fallback="" if data is None else "") + + +def _update_pickup(payload: PickupUpdateRequest, settings: Settings) -> Job: + data = Serializable(dict( + confirmation_number=payload.confirmation_number, + data=pickup_request(cast(PickupRequest, payload), settings, update=True), + ), _update_request_serializer) + + return Job(id="update_pickup", data=data, fallback="" if data is None else "") + + +def _get_pickup(update_response: str, payload: PickupUpdateRequest, settings: Settings) -> Job: + errors = parse_error_response(to_xml(bundle_xml([update_response])), settings) + data = None if any(errors) else f"/enab/{settings.customer_number}/pickuprequest/{payload.confirmation_number}/details" + + return Job(id="get_pickup", data=Serializable(data), fallback="" if data is None else "") def _update_request_serializer(request: dict) -> dict: diff --git a/extensions/canadapost/purplship/providers/canadapost/pickup/cancel_pickup.py b/extensions/canadapost/purplship/providers/canadapost/pickup/cancel_pickup.py index 0ae3d473ba..45078f8b63 100644 --- a/extensions/canadapost/purplship/providers/canadapost/pickup/cancel_pickup.py +++ b/extensions/canadapost/purplship/providers/canadapost/pickup/cancel_pickup.py @@ -1,6 +1,6 @@ from typing import Tuple, List from purplship.core.models import ( - PickupCancellationRequest, + PickupCancelRequest, Message, ConfirmationDetails, ) @@ -18,6 +18,7 @@ def parse_cancel_pickup_response( carrier_id=settings.carrier_id, carrier_name=settings.carrier_name, success=True, + operation="Cancel Pickup", ) if len(errors) == 0 else None @@ -26,5 +27,5 @@ def parse_cancel_pickup_response( return cancellation, errors -def cancel_pickup_request(payload: PickupCancellationRequest, _) -> Serializable[str]: +def cancel_pickup_request(payload: PickupCancelRequest, _) -> Serializable[str]: return Serializable(payload.confirmation_number) diff --git a/extensions/canadapost/purplship/providers/canadapost/pickup/pickup_request.py b/extensions/canadapost/purplship/providers/canadapost/pickup/pickup_request.py index bad7c971c6..8919e57cd3 100644 --- a/extensions/canadapost/purplship/providers/canadapost/pickup/pickup_request.py +++ b/extensions/canadapost/purplship/providers/canadapost/pickup/pickup_request.py @@ -38,7 +38,7 @@ def parse_pickup_response( ) -> Tuple[PickupDetails, List[Message]]: pickup = ( _extract_pickup_details(response, settings) - if len(response.xpath(".//*[local-name() = $name]", name="pickup-request-info")) + if len(response.xpath(".//*[local-name() = $name]", name="pickup-request-header")) > 0 else None ) @@ -46,14 +46,13 @@ def parse_pickup_response( def _extract_pickup_details(response: Element, settings: Settings) -> PickupDetails: - pickup_info = next( - build(PickupRequestInfoType, elt) - for elt in response.xpath( - ".//*[local-name() = $name]", name="pickup-request-info" - ) + header = next( + (build(PickupRequestHeaderType, elt) for elt in response.xpath(".//*[local-name() = $name]", name="pickup-request-header")) + ) + price = next( + (build(PickupRequestPriceType, elt) for elt in response.xpath(".//*[local-name() = $name]", name="pickup-request-price")), + None ) - header: PickupRequestHeaderType = pickup_info.pickup_request_header - price: PickupRequestPriceType = pickup_info.pickup_request_price price_amount = sum( [ @@ -62,7 +61,7 @@ def _extract_pickup_details(response: Element, settings: Settings) -> PickupDeta decimal(price.due_amount or 0.0), ], 0.0, - ) + ) if price is not None else None return PickupDetails( carrier_id=settings.carrier_id, @@ -71,7 +70,7 @@ def _extract_pickup_details(response: Element, settings: Settings) -> PickupDeta pickup_date=format_date(header.next_pickup_date), pickup_charge=ChargeDetails( name="Pickup fees", amount=decimal(price_amount), currency="CAD" - ), + ) if price is not None else None, ) @@ -90,6 +89,7 @@ def pickup_request( :param settings: Settings :return: Serializable[PickupRequest] """ + RequestType = PickupRequestUpdateDetailsType if update else PickupRequestDetailsType packages = Packages(payload.parcels, PackagePresets, required=["weight"]) heavy = any([p for p in packages if p.weight.KG > 23]) location_details = dict( @@ -98,7 +98,7 @@ def pickup_request( loading_dock_flag=payload.options.get("loading_dock_flag"), ) address = dict( - company=payload.address.company_name, + company=payload.address.company_name or "", address_line_1=concat_str( payload.address.address_line1, payload.address.address_line2, join=True ), @@ -106,13 +106,8 @@ def pickup_request( province=payload.address.state_code, postal_code=payload.address.postal_code, ) - contact = dict( - contact_name=payload.address.person_name, - email=payload.address.email, - contact_phone=payload.address.phone_number, - ) - request = PickupRequestDetailsType( + request = RequestType( customer_request_id=settings.customer_number, pickup_type=PickupType.ON_DEMAND.value, pickup_location=PickupLocationType( @@ -128,14 +123,12 @@ def pickup_request( else None, ), contact_info=ContactInfoType( - contact_name=contact["contact_name"], - email=contact["email"], - contact_phone=contact["contact_phone"], + contact_name=payload.address.person_name, + email=payload.address.email or "", + contact_phone=payload.address.phone_number, telephone_ext=None, - receive_email_updates_flag=(contact["email"] is not None), - ) - if any(contact.values()) - else None, + receive_email_updates_flag=(payload.address.email is not None), + ), location_details=LocationDetailsType( five_ton_flag=location_details["five_ton_flag"], loading_dock_flag=location_details["loading_dock_flag"], @@ -151,7 +144,7 @@ def pickup_request( pickup_volume=f"{len(packages) or 1}", pickup_times=PickupTimesType( on_demand_pickup_time=OnDemandPickupTimeType( - date=payload.date, + date=payload.pickup_date, preferred_time=payload.ready_time, closing_time=payload.closing_time, ), diff --git a/extensions/canadapost/purplship/providers/canadapost/shipment/__init__.py b/extensions/canadapost/purplship/providers/canadapost/shipment/__init__.py index 0714e620d1..1002a0089d 100644 --- a/extensions/canadapost/purplship/providers/canadapost/shipment/__init__.py +++ b/extensions/canadapost/purplship/providers/canadapost/shipment/__init__.py @@ -12,6 +12,10 @@ parse_non_contract_shipment_response, non_contract_shipment_request, ) +from purplship.providers.canadapost.shipment.void_shipment import ( + parse_void_shipment_response, + void_shipment_request, +) def parse_shipment_response( diff --git a/extensions/canadapost/purplship/providers/canadapost/shipment/contract_shipment.py b/extensions/canadapost/purplship/providers/canadapost/shipment/contract_shipment.py index da3092cfd3..636e37d587 100644 --- a/extensions/canadapost/purplship/providers/canadapost/shipment/contract_shipment.py +++ b/extensions/canadapost/purplship/providers/canadapost/shipment/contract_shipment.py @@ -64,7 +64,8 @@ def _extract_shipment(response: Element, settings: Settings) -> ShipmentDetails: carrier_name=settings.carrier_name, carrier_id=settings.carrier_id, tracking_number=info.tracking_pin, - label=label.text if len(errors) == 0 else None, + shipment_identifier=info.tracking_pin, + label=label.text if len(errors) == 0 else None ) @@ -184,8 +185,8 @@ def compute_amount(code: str, _: Any): customs=CustomsType( currency=Currency.AUD.value, conversion_from_cad=None, - reason_for_export=payload.customs.terms_of_trade, - other_reason=payload.customs.description, + reason_for_export=payload.customs.incoterm, + other_reason=payload.customs.content_description, duties_and_taxes_prepaid=payload.customs.duty.account_number, certificate_number=None, licence_number=None, diff --git a/extensions/canadapost/purplship/providers/canadapost/shipment/non_contract_shipment.py b/extensions/canadapost/purplship/providers/canadapost/shipment/non_contract_shipment.py index 9b78b9a141..ec4d8ec57b 100644 --- a/extensions/canadapost/purplship/providers/canadapost/shipment/non_contract_shipment.py +++ b/extensions/canadapost/purplship/providers/canadapost/shipment/non_contract_shipment.py @@ -59,6 +59,7 @@ def _extract_shipment(response: Element, settings: Settings) -> ShipmentDetails: carrier_name=settings.carrier_name, carrier_id=settings.carrier_id, tracking_number=info.tracking_pin, + shipment_identifier=info.tracking_pin, label=label.text if len(errors) == 0 else None, ) @@ -165,8 +166,8 @@ def compute_amount(code: str, _: Any): customs=CustomsType( currency=Currency.AUD.value, conversion_from_cad=None, - reason_for_export=payload.customs.terms_of_trade, - other_reason=payload.customs.description, + reason_for_export=payload.customs.incoterm, + other_reason=payload.customs.content_description, duties_and_taxes_prepaid=payload.customs.duty.account_number, certificate_number=None, licence_number=None, diff --git a/extensions/canadapost/purplship/providers/canadapost/shipment/void_shipment.py b/extensions/canadapost/purplship/providers/canadapost/shipment/void_shipment.py new file mode 100644 index 0000000000..ab716993c2 --- /dev/null +++ b/extensions/canadapost/purplship/providers/canadapost/shipment/void_shipment.py @@ -0,0 +1,29 @@ +from typing import List, Tuple +from purplship.core.models import ( + ShipmentCancelRequest, + ConfirmationDetails, + Message +) +from purplship.core.utils import ( + Element, + Serializable, +) +from purplship.providers.canadapost.error import parse_error_response +from purplship.providers.canadapost.utils import Settings + + +def parse_void_shipment_response(response: Element, settings: Settings) -> Tuple[ConfirmationDetails, List[Message]]: + errors = parse_error_response(response, settings) + success = len(errors) == 0 + confirmation: ConfirmationDetails = ConfirmationDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + success=success, + operation="Cancel Shipment", + ) if success else None + + return confirmation, errors + + +def void_shipment_request(payload: ShipmentCancelRequest, _) -> Serializable[str]: + return Serializable(payload.shipment_identifier) diff --git a/extensions/canadapost/purplship/providers/canadapost/utils.py b/extensions/canadapost/purplship/providers/canadapost/utils.py index 1c12a62434..b5e372b2be 100644 --- a/extensions/canadapost/purplship/providers/canadapost/utils.py +++ b/extensions/canadapost/purplship/providers/canadapost/utils.py @@ -11,6 +11,7 @@ class Settings(BaseSettings): password: str customer_number: str contract_id: str = None + language: str = "en" id: str = None @property diff --git a/extensions/canadapost/setup.py b/extensions/canadapost/setup.py index 938b6321f8..b6adb0776b 100644 --- a/extensions/canadapost/setup.py +++ b/extensions/canadapost/setup.py @@ -2,7 +2,7 @@ setup( name="purplship.canadapost", - version="2020.9.0", + version="2020.10.0", description="Multi-carrier shipping API integration with python", url="https://github.com/PurplShip/purplship", author="Purplship Team", diff --git a/extensions/canpar/purplship/mappers/canpar/__init__.py b/extensions/canpar/purplship/mappers/canpar/__init__.py new file mode 100644 index 0000000000..13e5daeac5 --- /dev/null +++ b/extensions/canpar/purplship/mappers/canpar/__init__.py @@ -0,0 +1,3 @@ +from purplship.mappers.canpar.mapper import Mapper +from purplship.mappers.canpar.proxy import Proxy +from purplship.mappers.canpar.settings import Settings diff --git a/extensions/canpar/purplship/mappers/canpar/mapper.py b/extensions/canpar/purplship/mappers/canpar/mapper.py new file mode 100644 index 0000000000..3a4d3f22eb --- /dev/null +++ b/extensions/canpar/purplship/mappers/canpar/mapper.py @@ -0,0 +1,121 @@ +from typing import List, Tuple +from purplship.core.utils.serializable import Serializable, Deserializable +from purplship.api.mapper import Mapper as BaseMapper +from purplship.core.models import ( + ShipmentRequest, + TrackingRequest, + Message, + TrackingDetails, + RateDetails, + RateRequest, + ShipmentDetails, + AddressValidationRequest, + AddressValidationDetails, + PickupRequest, + PickupUpdateRequest, + PickupDetails, + PickupCancelRequest, + ConfirmationDetails, + ShipmentCancelRequest +) +from purplship.core.utils import Envelope, Pipeline +from purplship.providers.canpar import ( + rate_shipment_request, + parse_rate_shipment_response, + track_by_barcode, + parse_track_response, + create_shipment_pipeline, + parse_shipment_response, + void_shipment_request, + parse_void_shipment_response, + cancel_pickup_request, + parse_cancel_pickup_response, + schedule_pickup_request, + parse_schedule_pickup_response, + update_pickup_request, + search_canada_post, + parse_search_response, +) +from purplship.mappers.canpar.settings import Settings + + +class Mapper(BaseMapper): + settings: Settings + + """Request Mappers""" + + def create_address_validation_request(self, payload: AddressValidationRequest) -> Serializable[Envelope]: + return search_canada_post(payload, self.settings) + + def create_rate_request( + self, payload: RateRequest + ) -> Serializable[Envelope]: + return rate_shipment_request(payload, self.settings) + + def create_tracking_request( + self, payload: TrackingRequest + ) -> Serializable[Envelope]: + return track_by_barcode(payload, self.settings) + + def create_shipment_request( + self, payload: ShipmentRequest + ) -> Serializable[Pipeline]: + return create_shipment_pipeline(payload, self.settings) + + def create_cancel_shipment_request(self, payload: ShipmentCancelRequest) -> Serializable: + return void_shipment_request(payload, self.settings) + + def create_pickup_request(self, payload: PickupRequest) -> Serializable[Envelope]: + return schedule_pickup_request(payload, self.settings) + + def create_pickup_update_request( + self, payload: PickupUpdateRequest + ) -> Serializable[Pipeline]: + return update_pickup_request(payload, self.settings) + + def create_cancel_pickup_request( + self, payload: PickupCancelRequest + ) -> Serializable[Envelope]: + return cancel_pickup_request(payload, self.settings) + + """Response Parsers""" + + def parse_address_validation_response( + self, response: Deserializable[str] + ) -> Tuple[AddressValidationDetails, List[Message]]: + return parse_search_response(response.deserialize(), self.settings) + + def parse_rate_response( + self, response: Deserializable[str] + ) -> Tuple[List[RateDetails], List[Message]]: + return parse_rate_shipment_response(response.deserialize(), self.settings) + + def parse_tracking_response( + self, response: Deserializable[str] + ) -> Tuple[List[TrackingDetails], List[Message]]: + return parse_track_response(response.deserialize(), self.settings) + + def parse_shipment_response( + self, response: Deserializable[str] + ) -> Tuple[ShipmentDetails, List[Message]]: + return parse_shipment_response(response.deserialize(), self.settings) + + def parse_cancel_shipment_response( + self, response: Deserializable[str] + ) -> Tuple[ConfirmationDetails, List[Message]]: + return parse_void_shipment_response(response.deserialize(), self.settings) + + def parse_pickup_response( + self, response: Deserializable[str] + ) -> Tuple[PickupDetails, List[Message]]: + return parse_schedule_pickup_response(response.deserialize(), self.settings) + + def parse_pickup_update_response( + self, response: Deserializable[str] + ) -> Tuple[PickupDetails, List[Message]]: + return parse_schedule_pickup_response(response.deserialize(), self.settings) + + def parse_cancel_pickup_response( + self, response: Deserializable[str] + ) -> Tuple[ConfirmationDetails, List[Message]]: + return parse_cancel_pickup_response(response.deserialize(), self.settings) diff --git a/extensions/canpar/purplship/mappers/canpar/proxy.py b/extensions/canpar/purplship/mappers/canpar/proxy.py new file mode 100644 index 0000000000..9ece83690e --- /dev/null +++ b/extensions/canpar/purplship/mappers/canpar/proxy.py @@ -0,0 +1,130 @@ +from typing import List, Any +from purplship.core.utils import ( + Serializable, + Deserializable, + Envelope, + Pipeline, + Job, + to_xml, + bundle_xml, + request as http, + exec_parrallel +) +from purplship.mappers.canpar.settings import Settings +from purplship.api.proxy import Proxy as BaseProxy + + +class Proxy(BaseProxy): + settings: Settings + + def _send_request( + self, path: str, soapaction: str, request: Serializable[Any] + ) -> str: + return http( + url=f"{self.settings.server_url}{path}", + data=bytearray(request.serialize(), "utf-8"), + headers={ + "Content-Type": "text/xml; charset=utf-8", + "soapaction": soapaction, + }, + method="POST", + ) + + def validate_address(self, request: Serializable[Envelope]) -> Deserializable[str]: + response = self._send_request( + path="/CanparRatingService.CanparRatingServiceHttpSoap12Endpoint/", + soapaction="urn:searchCanadaPost", + request=request, + ) + + return Deserializable(response, to_xml) + + def get_rates(self, request: Serializable[Envelope]) -> Deserializable[str]: + response = self._send_request( + path="/CanparRatingService.CanparRatingServiceHttpSoap12Endpoint/", + soapaction="urn:rateShipment", + request=request, + ) + + return Deserializable(response, to_xml) + + def get_tracking(self, request: Serializable[List[Envelope]]) -> Deserializable[str]: + """ + get_tracking make parallel request for each TrackRequest + """ + + def get_tracking(track_request: str): + return self._send_request( + path="/CanparAddonsService.CanparAddonsServiceHttpSoap12Endpoint/", + soapaction="urn:trackByBarcodeV2", + request=Serializable(track_request), + ) + + response: List[str] = exec_parrallel(get_tracking, request.serialize()) + + return Deserializable(bundle_xml(xml_strings=response), to_xml) + + def create_shipment(self, request: Serializable[Envelope]) -> Deserializable[str]: + def process(job: Job): + if job.data is None: + return job.fallback + + return self._send_request( + path="/CanshipBusinessService.CanshipBusinessServiceHttpSoap12Endpoint/", + request=job.data, + soapaction=dict( + process="urn:processShipment", + get_label="urn:getLabels", + )[job.id], + ) + + pipeline: Pipeline = request.serialize() + response = pipeline.apply(process) + + return Deserializable(bundle_xml(response), to_xml) + + def cancel_shipment(self, request: Serializable[Envelope]) -> Deserializable[str]: + response = self._send_request( + path="/CanshipBusinessService.CanshipBusinessServiceHttpSoap12Endpoint/", + soapaction="urn:voidShipment", + request=request, + ) + + return Deserializable(response, to_xml) + + def schedule_pickup(self, request: Serializable[Envelope]) -> Deserializable[str]: + response = self._send_request( + path="/CanparAddonsService.CanparAddonsServiceHttpSoap12Endpoint/", + soapaction="urn:schedulePickupV2", + request=request, + ) + + return Deserializable(response, to_xml) + + def modify_pickup(self, request: Serializable[Envelope]) -> Deserializable[str]: + def process(job: Job): + if job.data is None: + return job.fallback + + return self._send_request( + path="/CanparAddonsService.CanparAddonsServiceHttpSoap12Endpoint/", + request=job.data, + soapaction=dict( + cancel="urn:cancelPickup", + schedule="urn:schedulePickupV2", + )[job.id], + ) + + pipeline: Pipeline = request.serialize() + response = pipeline.apply(process) + + return Deserializable(bundle_xml(response), to_xml) + + def cancel_pickup(self, request: Serializable[Envelope]) -> Deserializable[str]: + response = self._send_request( + path="/CanparAddonsService.CanparAddonsServiceHttpSoap12Endpoint/", + soapaction="urn:cancelPickup", + request=request, + ) + + return Deserializable(response, to_xml) diff --git a/extensions/canpar/purplship/mappers/canpar/settings.py b/extensions/canpar/purplship/mappers/canpar/settings.py new file mode 100644 index 0000000000..1aba6b4b02 --- /dev/null +++ b/extensions/canpar/purplship/mappers/canpar/settings.py @@ -0,0 +1,15 @@ +"""Purplship Canpar client settings.""" + +import attr +from purplship.providers.canpar import Settings as BaseSettings + + +@attr.s(auto_attribs=True) +class Settings(BaseSettings): + """Canpar connection settings.""" + + user_id: str + password: str + id: str = None + test: bool = False + carrier_id: str = "canpar" diff --git a/extensions/canpar/purplship/providers/canpar/__init__.py b/extensions/canpar/purplship/providers/canpar/__init__.py new file mode 100644 index 0000000000..0ecf27bedb --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/__init__.py @@ -0,0 +1,17 @@ +from purplship.providers.canpar.utils import Settings +from purplship.providers.canpar.rate import rate_shipment_request, parse_rate_shipment_response +from purplship.providers.canpar.track import track_by_barcode, parse_track_response +from purplship.providers.canpar.address import search_canada_post, parse_search_response +from purplship.providers.canpar.shipment import ( + create_shipment_pipeline, + parse_shipment_response, + void_shipment_request, + parse_void_shipment_response +) +from purplship.providers.canpar.pickup import ( + cancel_pickup_request, + parse_cancel_pickup_response, + schedule_pickup_request, + parse_schedule_pickup_response, + update_pickup_request +) diff --git a/extensions/canpar/purplship/providers/canpar/address.py b/extensions/canpar/purplship/providers/canpar/address.py new file mode 100644 index 0000000000..3e7b7605be --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/address.py @@ -0,0 +1,69 @@ +from typing import List, Tuple +from pycanpar.CanparRatingService import ( + searchCanadaPost, + SearchCanadaPostRq, + Address as CanparAddress +) +from purplship.core.models import ( + AddressValidationDetails, + AddressValidationRequest, + Message, + Address +) +from purplship.core.utils import ( + create_envelope, + Element, + Envelope, + Serializable, + concat_str, + build +) +from purplship.providers.canpar.error import parse_error_response +from purplship.providers.canpar.utils import Settings, default_request_serializer + + +def parse_search_response(response: Element, settings: Settings) -> Tuple[AddressValidationDetails, List[Message]]: + errors = parse_error_response(response, settings) + address_node = next(iter(response.xpath(".//*[local-name() = $name]", name="address")), None) + address = build(CanparAddress, address_node) + success = len(errors) == 0 + validation_details = AddressValidationDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + success=success, + complete_address=Address( + postal_code=address.postal_code, + city=address.city, + company_name=address.name, + country_code=address.country, + email=address.email, + state_code=address.province, + residential=address.residential, + address_line1=address.address_line_1, + address_line2=concat_str(address.address_line_2, address.address_line_3, join=True) + ) + ) if success else None + + return validation_details, errors + + +def search_canada_post(payload: AddressValidationRequest, settings: Settings) -> Serializable[Envelope]: + + request = create_envelope( + body_content=searchCanadaPost( + request=SearchCanadaPostRq( + city=payload.address.city or "", + password=settings.password, + postal_code=payload.address.postal_code or "", + province=payload.address.state_code or "", + street_direction="", + street_name=concat_str(payload.address.address_line1, payload.address.address_line2, join=True) or "", + street_num="", + street_type="", + user_id=settings.user_id, + validate_only=True + ) + ) + ) + + return Serializable(request, default_request_serializer) diff --git a/extensions/canpar/purplship/providers/canpar/error.py b/extensions/canpar/purplship/providers/canpar/error.py new file mode 100644 index 0000000000..a5c167d186 --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/error.py @@ -0,0 +1,20 @@ +from typing import List +from purplship.core.models import Message +from purplship.core.utils import Element, extract_fault +from purplship.providers.canpar.utils import Settings + + +def parse_error_response(response: Element, settings: Settings) -> List[Message]: + errors: List[Element] = response.xpath(".//*[local-name() = $name]", name="error") + return ( + extract_fault(response, settings) + + [ + Message( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + message=error.text, + ) + for error in errors + if error.text is not None + ] + ) diff --git a/extensions/canpar/purplship/providers/canpar/pickup/__init__.py b/extensions/canpar/purplship/providers/canpar/pickup/__init__.py new file mode 100644 index 0000000000..0c657a1f6b --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/pickup/__init__.py @@ -0,0 +1,57 @@ +from typing import Optional +from functools import partial +from purplship.core.models import ( + PickupRequest, + PickupUpdateRequest, + PickupCancelRequest +) +from purplship.core.utils import ( + Pipeline, + Job, + Serializable, + to_xml +) +from purplship.providers.canpar.utils import Settings +from purplship.providers.canpar.error import parse_error_response +from purplship.providers.canpar.pickup.cancel import cancel_pickup_request, parse_cancel_pickup_response +from purplship.providers.canpar.pickup.schedule import schedule_pickup_request, parse_schedule_pickup_response + + +def update_pickup_request(payload: PickupUpdateRequest, settings: Settings) -> Serializable[Pipeline]: + """ + Modify a pickup request + Steps + 1 - cancel former pickup + 2 - create a new one + :param payload: PickupUpdateRequest + :param settings: Settings + :return: Serializable[Pipeline] + """ + request: Pipeline = Pipeline( + cancel=lambda *_: _cancel_pickup(payload, settings), + schedule=partial(_create_pickup, payload=payload, settings=settings) + ) + + return Serializable(request) + + +def _cancel_pickup(payload: PickupUpdateRequest, settings: Settings) -> Job: + data = PickupCancelRequest( + confirmation_number=payload.confirmation_number, + address=payload.address, + pickup_date=payload.pickup_date, + reason='change pickup', + ) + + return Job(id='cancel', data=cancel_pickup_request(data, settings)) + + +def _create_pickup(cancel_response: str, payload: PickupUpdateRequest, settings: Settings) -> Job: + errors = parse_error_response(to_xml(cancel_response), settings) + canceled = len(errors) == 0 + data: Optional[PickupRequest] = ( + schedule_pickup_request(payload, settings) if canceled else None + ) + fallback: Optional[str] = None if canceled else '' + + return Job(id='schedule', data=data, fallback=fallback) diff --git a/extensions/canpar/purplship/providers/canpar/pickup/cancel.py b/extensions/canpar/purplship/providers/canpar/pickup/cancel.py new file mode 100644 index 0000000000..5a18393c78 --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/pickup/cancel.py @@ -0,0 +1,46 @@ +from typing import List, Tuple +from pycanpar.CanparAddonsService import ( + cancelPickup, + CancelPickupRq, +) +from purplship.core.models import ( + PickupCancelRequest, + ConfirmationDetails, + Message, +) +from purplship.core.utils import ( + create_envelope, + Envelope, + Element, + Serializable +) +from purplship.providers.canpar.error import parse_error_response +from purplship.providers.canpar.utils import Settings, default_request_serializer + + +def parse_cancel_pickup_response(response: Element, settings: Settings) -> Tuple[ConfirmationDetails, List[Message]]: + errors = parse_error_response(response, settings) + success = len(errors) == 0 + confirmation: ConfirmationDetails = ConfirmationDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + success=success, + operation="Cancel Pickup", + ) if success else None + + return confirmation, errors + + +def cancel_pickup_request(payload: PickupCancelRequest, settings: Settings) -> Serializable[Envelope]: + + request = create_envelope( + body_content=cancelPickup( + request=CancelPickupRq( + id=int(payload.confirmation_number), + password=settings.password, + user_id=settings.user_id + ) + ) + ) + + return Serializable(request, default_request_serializer) diff --git a/extensions/canpar/purplship/providers/canpar/pickup/schedule.py b/extensions/canpar/purplship/providers/canpar/pickup/schedule.py new file mode 100644 index 0000000000..5a469abbef --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/pickup/schedule.py @@ -0,0 +1,80 @@ +from typing import Tuple, List +from pycanpar.CanparAddonsService import ( + schedulePickupV2, + SchedulePickupV2Rq, + PickupV2, + Address +) +from purplship.core.models import ( + PickupRequest, + PickupDetails, + Message +) +from purplship.core.utils import ( + Envelope, + Element, + create_envelope, + Serializable, + format_datetime, + build +) +from purplship.core.units import Packages +from purplship.providers.canpar.error import parse_error_response +from purplship.providers.canpar.utils import Settings, default_request_serializer +from purplship.providers.canpar.units import WeightUnit + + +def parse_schedule_pickup_response(response: Element, settings: Settings) -> Tuple[PickupDetails, List[Message]]: + pickup_node = next(iter(response.xpath(".//*[local-name() = $name]", name="pickup")), None) + pickup = build(PickupV2, pickup_node) + details: PickupDetails = PickupDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + confirmation_number=str(pickup.id), + pickup_date=format_datetime(pickup.pickup_date, '%Y-%m-%dT%H:%M:%S') + ) + + return details, parse_error_response(response, settings) + + +def schedule_pickup_request(payload: PickupRequest, settings: Settings) -> Serializable[Envelope]: + packages = Packages(payload.parcels) + + request = create_envelope( + body_content=schedulePickupV2( + request=SchedulePickupV2Rq( + password=settings.password, + pickup=PickupV2( + collect=None, + comments=payload.instruction, + created_by=payload.address.person_name, + pickup_address=Address( + address_line_1=payload.address.address_line1, + address_line_2=payload.address.address_line2, + address_line_3=None, + attention=payload.address.person_name, + city=payload.address.city, + country=payload.address.country_code, + email=payload.address.email, + extension=None, + name=payload.address.company_name, + phone=payload.address.phone_number, + postal_code=payload.address.postal_code, + province=payload.address.state_code, + residential=payload.address.residential, + ), + pickup_date=format_datetime( + f"{payload.pickup_date} {payload.ready_time}", '%Y-%m-%d %H:%M', '%Y-%m-%dT%H:%M:%S' + ), + pickup_location=payload.package_location, + pickup_phone=payload.address.phone_number, + shipper_num=None, + unit_of_measure=WeightUnit.LB.value, + weight=packages.weight.LB + ), + user_id=settings.user_id + ) + ) + ) + + return Serializable(request, default_request_serializer) diff --git a/extensions/canpar/purplship/providers/canpar/rate.py b/extensions/canpar/purplship/providers/canpar/rate.py new file mode 100644 index 0000000000..eb159f9087 --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/rate.py @@ -0,0 +1,164 @@ +from datetime import datetime +from typing import List, Tuple, Optional +from pycanpar.CanparRatingService import ( + rateShipment, + RateShipmentRq, + Shipment, + Package, + Address, +) +from purplship.core.models import ( + RateRequest, + RateDetails, + Message, + ChargeDetails +) +from purplship.core.units import Packages +from purplship.core.utils import Serializable, Envelope, create_envelope, Element, build, decimal +from purplship.providers.canpar.error import parse_error_response +from purplship.providers.canpar.utils import Settings, default_request_serializer +from purplship.providers.canpar.units import WeightUnit, DimensionUnit, Option, Service, Charges + + +def parse_rate_shipment_response(response: Element, settings: Settings) -> Tuple[List[RateDetails], List[Message]]: + shipment_nodes = response.xpath(".//*[local-name() = $name]", name="shipment") + rates: List[RateDetails] = [ + _extract_rate_details(node, settings) for node in shipment_nodes + ] + return rates, parse_error_response(response, settings) + + +def _extract_rate_details(node: Element, settings: Settings) -> RateDetails: + shipment = build(Shipment, node) + surcharges = [ + ChargeDetails( + name=charge.value, + amount=decimal(getattr(shipment, charge.name)), + currency='CAD' + ) + for charge in list(Charges) + if decimal(getattr(shipment, charge.name)) > 0.0 + ] + taxes = [ + ChargeDetails( + name=f'{getattr(shipment, code)} Tax Charge', + amount=decimal(decimal(charge)), + currency='CAD' + ) + for code, charge in [('tax_code_1', shipment.tax_charge_1), ('tax_code_2', shipment.tax_charge_2)] + if decimal(charge) > 0.0 + ] + + return RateDetails( + carrier_name=settings.carrier_name, + carrier_id=settings.carrier_id, + currency="CAD", + transit_days=shipment.transit_time, + service=Service(shipment.service_type).name, + base_charge=decimal(shipment.freight_charge), + total_charge=sum([c.amount for c in (surcharges + taxes)], 0.0), + duties_and_taxes=sum([t.amount for t in taxes], 0.0), + extra_charges=(surcharges + taxes), + ) + + +def rate_shipment_request(payload: RateRequest, settings: Settings) -> Serializable[Envelope]: + packages = Packages(payload.parcels) + service_type: Optional[str] = next( + (Service[key].value for key in payload.services if key in Service.__members__), + None + ) + options = { + Option[key].name: Option[key].value for key in payload.options.keys() + if key in Option.__members__ + } + premium: Optional[bool] = next((True for option in options.keys() if option in [ + Option.canpar_ten_am.value, + Option.canpar_noon.value, + Option.canpar_saturday.value, + ]), None) + + request = create_envelope( + body_content=rateShipment( + request=RateShipmentRq( + apply_association_discount=False, + apply_individual_discount=False, + apply_invoice_discount=False, + password=settings.password, + shipment=Shipment( + cod_type=options.get('canpar_cash_on_delivery'), + delivery_address=Address( + address_line_1=payload.recipient.address_line1, + address_line_2=payload.recipient.address_line2, + address_line_3=None, + attention=payload.recipient.person_name, + city=payload.recipient.city, + country=payload.recipient.country_code, + email=payload.recipient.email, + extension=None, + name=payload.recipient.company_name, + phone=payload.recipient.phone_number, + postal_code=payload.recipient.postal_code, + province=payload.recipient.state_code, + residential=payload.recipient.residential, + ), + description=None, + dg=('canpar_dangerous_goods' in options) or None, + dimention_unit=DimensionUnit.IN.value, + handling=None, + handling_type=None, + instruction=None, + nsr=( + options.get('canpar_no_signature_required') or options.get('canpar_not_no_signature_required') + ), + packages=[ + Package( + alternative_reference=None, + cod=None, + cost_centre=None, + declared_value=None, + height=pkg.height.CM, + length=pkg.length.CM, + lg=None, + reference=None, + reported_weight=pkg.weight.LB, + store_num=None, + width=pkg.width.CM, + xc=('canpar_extra_care' in options) or None + ) for pkg in packages + ], + pickup_address=Address( + address_line_1=payload.shipper.address_line1, + address_line_2=payload.shipper.address_line2, + address_line_3=None, + attention=payload.shipper.person_name, + city=payload.shipper.city, + country=payload.shipper.country_code, + email=payload.shipper.email, + extension=None, + name=payload.shipper.company_name, + phone=payload.shipper.phone_number, + postal_code=payload.shipper.postal_code, + province=payload.shipper.state_code, + residential=payload.shipper.residential, + ), + premium=premium, + proforma=None, + reported_weight_unit=WeightUnit.LB.value, + send_email_to_delivery=payload.recipient.email, + send_email_to_pickup=payload.shipper.email, + service_type=service_type, + shipper_num=None, + shipping_date=datetime.today().strftime('%Y-%m-%dT%H:%M:%S'), + subtotal=None, + subtotal_with_handling=None, + total=None, + total_with_handling=None, + user_id=None, + ), + user_id=settings.user_id + ) + ) + ) + + return Serializable(request, default_request_serializer) diff --git a/extensions/canpar/purplship/providers/canpar/shipment/__init__.py b/extensions/canpar/purplship/providers/canpar/shipment/__init__.py new file mode 100644 index 0000000000..6fda2130fc --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/shipment/__init__.py @@ -0,0 +1,44 @@ +from purplship.providers.canpar.shipment.process import process_shipment_request, parse_shipment_response +from purplship.providers.canpar.shipment.void import void_shipment_request, parse_void_shipment_response +from purplship.providers.canpar.shipment.label import get_label_request, LabelRequest +from functools import partial +from pycanpar.CanshipBusinessService import Shipment +from purplship.core.models import ShipmentRequest +from purplship.core.utils import ( + Serializable, + Pipeline, + Job, + to_xml, + build +) +from purplship.providers.canpar.utils import Settings + + +def create_shipment_pipeline(payload: ShipmentRequest, settings: Settings) -> Serializable[Pipeline]: + + request: Pipeline = Pipeline( + process=lambda *_: _process_shipment(payload, settings), + get_label=partial(_get_label, settings=settings) + ) + + return Serializable(request) + + +def _process_shipment(payload: ShipmentRequest, settings: Settings) -> Job: + + return Job(id="process", data=process_shipment_request(payload, settings)) + + +def _get_label(shipment_response: str, settings: Settings) -> Job: + response = to_xml(shipment_response) + shipment = build( + Shipment, next(iter(response.xpath(".//*[local-name() = $name]", name="shipment")), None) + ) + success = (shipment is not None and shipment.id is not None) + data = ( + get_label_request(LabelRequest(shipment_id=shipment.id), settings) + if success else + None + ) + + return Job(id="get_label", data=data, fallback=("" if not success else None)) diff --git a/extensions/canpar/purplship/providers/canpar/shipment/label.py b/extensions/canpar/purplship/providers/canpar/shipment/label.py new file mode 100644 index 0000000000..f9d489f751 --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/shipment/label.py @@ -0,0 +1,34 @@ +from jstruct import struct +from pycanpar.CanshipBusinessService import ( + GetLabelsAdvancedRq, + getLabelsAdvanced +) +from purplship.core.utils import ( + create_envelope, + Serializable, + Envelope +) +from purplship.providers.canpar.utils import Settings, default_request_serializer + + +@struct +class LabelRequest: + shipment_id: str + thermal: bool = False + + +def get_label_request(payload: LabelRequest, settings: Settings) -> Serializable[Envelope]: + + request = create_envelope( + body_content=getLabelsAdvanced( + request=GetLabelsAdvancedRq( + horizontal=False, + id=payload.shipment_id, + password=settings.password, + thermal=payload.thermal, + user_id=settings.user_id + ) + ) + ) + + return Serializable(request, default_request_serializer) diff --git a/extensions/canpar/purplship/providers/canpar/shipment/process.py b/extensions/canpar/purplship/providers/canpar/shipment/process.py new file mode 100644 index 0000000000..b2a5bbc52a --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/shipment/process.py @@ -0,0 +1,150 @@ +from datetime import datetime +from typing import List, Tuple, Optional +from pycanpar.CanshipBusinessService import ( + processShipment, + ProcessShipmentRq, + Shipment, + Address, + Package, +) +from purplship.core.models import ( + Message, + ShipmentRequest, + ShipmentDetails, +) +from purplship.core.utils import ( + Serializable, + Element, + create_envelope, + Envelope, + build, +) +from purplship.core.units import Packages +from purplship.providers.canpar.error import parse_error_response +from purplship.providers.canpar.utils import Settings, default_request_serializer +from purplship.providers.canpar.units import WeightUnit, DimensionUnit, Option, Service +from purplship.providers.canpar.rate import _extract_rate_details + + +def parse_shipment_response(response: Element, settings: Settings) -> Tuple[ShipmentDetails, List[Message]]: + shipment = build( + Shipment, next(iter(response.xpath(".//*[local-name() = $name]", name="shipment")), None) + ) + success = (shipment is not None and shipment.id is not None) + shipment_details = _extract_shipment_details(response, settings) if success else None + + return shipment_details, parse_error_response(response, settings) + + +def _extract_shipment_details(response: Element, settings: Settings) -> ShipmentDetails: + shipment_node = next(iter(response.xpath(".//*[local-name() = $name]", name="shipment")), None) + label = next(iter(response.xpath(".//*[local-name() = $name]", name="labels")), None) + shipment = build(Shipment, shipment_node) + tracking_number = next(iter(shipment.packages), Package()).barcode + + return ShipmentDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + label=label.text, + tracking_number=tracking_number, + shipment_identifier=str(shipment.id), + selected_rate=_extract_rate_details(shipment_node, settings), + ) + + +def process_shipment_request(payload: ShipmentRequest, settings: Settings) -> Serializable[List[Envelope]]: + packages = Packages(payload.parcels) + service_type: Optional[str] = ( + Service[payload.service] if payload.service in Service.__members__ else None + ) + options = { + Option[key].name: Option[key].value for key in payload.options.keys() + if key in Option.__members__ + } + premium: Optional[bool] = next((True for option in options.keys() if option in [ + Option.canpar_ten_am.value, + Option.canpar_noon.value, + Option.canpar_saturday.value, + ]), None) + + request = create_envelope( + body_content=processShipment( + request=ProcessShipmentRq( + password=settings.password, + shipment=Shipment( + cod_type=options.get('canpar_cash_on_delivery'), + delivery_address=Address( + address_line_1=payload.recipient.address_line1, + address_line_2=payload.recipient.address_line2, + address_line_3=None, + attention=payload.recipient.person_name, + city=payload.recipient.city, + country=payload.recipient.country_code, + email=payload.recipient.email, + extension=None, + name=payload.recipient.company_name, + phone=payload.recipient.phone_number, + postal_code=payload.recipient.postal_code, + province=payload.recipient.state_code, + residential=payload.recipient.residential, + ), + description=None, + dg=('canpar_dangerous_goods' in options) or None, + dimention_unit=DimensionUnit.IN.value, + handling=None, + handling_type=None, + instruction=None, + nsr=( + options.get('canpar_no_signature_required') or options.get('canpar_not_no_signature_required') + ), + packages=[ + Package( + alternative_reference=None, + cod=None, + cost_centre=None, + declared_value=None, + height=pkg.height.CM, + length=pkg.length.CM, + lg=None, + reference=None, + reported_weight=pkg.weight.LB, + store_num=None, + width=pkg.width.CM, + xc=('canpar_extra_care' in options) or None + ) for pkg in packages + ], + pickup_address=Address( + address_line_1=payload.shipper.address_line1, + address_line_2=payload.shipper.address_line2, + address_line_3=None, + attention=payload.shipper.person_name, + city=payload.shipper.city, + country=payload.shipper.country_code, + email=payload.shipper.email, + extension=None, + name=payload.shipper.company_name, + phone=payload.shipper.phone_number, + postal_code=payload.shipper.postal_code, + province=payload.shipper.state_code, + residential=payload.shipper.residential, + ), + premium=premium, + proforma=None, + reported_weight_unit=WeightUnit.LB.value, + send_email_to_delivery=payload.recipient.email, + send_email_to_pickup=payload.shipper.email, + service_type=service_type, + shipper_num=None, + shipping_date=datetime.today().strftime('%Y-%m-%dT%H:%M:%S'), + subtotal=None, + subtotal_with_handling=None, + total=None, + total_with_handling=None, + user_id=None, + ), + user_id=settings.user_id + ) + ) + ) + + return Serializable(request, default_request_serializer) diff --git a/extensions/canpar/purplship/providers/canpar/shipment/void.py b/extensions/canpar/purplship/providers/canpar/shipment/void.py new file mode 100644 index 0000000000..408e1b7242 --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/shipment/void.py @@ -0,0 +1,46 @@ +from typing import List, Tuple +from pycanpar.CanshipBusinessService import ( + voidShipment, + VoidShipmentRq, +) +from purplship.core.models import ( + ShipmentCancelRequest, + ConfirmationDetails, + Message +) +from purplship.core.utils import ( + create_envelope, + Envelope, + Element, + Serializable, +) +from purplship.providers.canpar.error import parse_error_response +from purplship.providers.canpar.utils import Settings, default_request_serializer + + +def parse_void_shipment_response(response: Element, settings: Settings) -> Tuple[ConfirmationDetails, List[Message]]: + errors = parse_error_response(response, settings) + success = len(errors) == 0 + confirmation: ConfirmationDetails = ConfirmationDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + success=success, + operation="Cancel Shipment" + ) if success else None + + return confirmation, errors + + +def void_shipment_request(payload: ShipmentCancelRequest, settings: Settings) -> Serializable[Envelope]: + + request = create_envelope( + body_content=voidShipment( + request=VoidShipmentRq( + id=int(payload.shipment_identifier), + password=settings.password, + user_id=settings.user_id + ) + ) + ) + + return Serializable(request, default_request_serializer) diff --git a/extensions/canpar/purplship/providers/canpar/track.py b/extensions/canpar/purplship/providers/canpar/track.py new file mode 100644 index 0000000000..538317da1c --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/track.py @@ -0,0 +1,90 @@ +from typing import List, Tuple, cast +from pycanpar.CanparAddonsService import ( + trackByBarcodeV2, + TrackByBarcodeV2Rq, + TrackingResult, + TrackingEvent as CanparTrackingEvent, + Address +) +from purplship.core.models import ( + Message, + TrackingRequest, + TrackingDetails, + TrackingEvent, +) +from purplship.core.utils import ( + Serializable, + Element, + create_envelope, + Envelope, + build, + format_date, + format_time, +) +from purplship.providers.canpar.error import parse_error_response +from purplship.providers.canpar.utils import Settings, default_request_serializer + + +def parse_track_response(response: Element, settings: Settings) -> Tuple[List[TrackingDetails], List[Message]]: + results = response.xpath(".//*[local-name() = $name]", name="result") + details: List[TrackingDetails] = [ + _extract_tracking_details(result, settings) for result in results + ] + + return details, parse_error_response(response, settings) + + +def _extract_tracking_details(node: Element, settings: Settings) -> TrackingDetails: + result = build(TrackingResult, node) + is_en = settings.language == "en" + events = [ + TrackingEvent( + date=format_date(event.local_date_time, '%Y%m%d %H%M%S'), + description=(event.code_description_en if is_en else event.code_description_fr), + location=_format_location(event.address), + code=event.code, + time=format_time(event.local_date_time, '%Y%m%d %H%M%S'), + ) for event in cast(List[CanparTrackingEvent], result.events) + ] + + return TrackingDetails( + carrier_name=settings.carrier_name, + carrier_id=settings.carrier_id, + tracking_number=result.barcode, + events=events, + delivered=any(event.code == 'DEL' for event in events) + ) + + +def _format_location(address: Address) -> str: + details = [ + address.address_line_1, + address.address_line_2, + address.city, + address.province, + address.country + ] + return ", ".join([detail for detail in details if detail is not None and detail != ""]) + + +def track_by_barcode(payload: TrackingRequest, _) -> Serializable[List[Envelope]]: + + request = [ + create_envelope( + body_content=trackByBarcodeV2( + request=TrackByBarcodeV2Rq( + barcode=barcode, + filter=None, + track_shipment=True + ) + ) + ) for barcode in payload.tracking_numbers + ] + + return Serializable(request, _request_serializer) + + +def _request_serializer(envelopes: List[Envelope]) -> List[str]: + return [ + default_request_serializer(envelope) for envelope in envelopes + ] diff --git a/extensions/canpar/purplship/providers/canpar/units.py b/extensions/canpar/purplship/providers/canpar/units.py new file mode 100644 index 0000000000..0b9df09c77 --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/units.py @@ -0,0 +1,63 @@ +from enum import Enum, Flag +from purplship.core.units import PackagePreset as BasePackagePreset +from dataclasses import dataclass + + +@dataclass +class PackagePreset(BasePackagePreset): + dimension_unit: str = "IN" + weight_unit: str = "LB" + + +class WeightUnit(Flag): + LB = 'L' + KG = 'K' + + +class DimensionUnit(Enum): + IN = 'I' + CM = 'C' + + +class Charges(Flag): + cod_charge = 'Cash On Delivery' + cos_charge = 'Chain of Signature' + dg_charge = 'Dangerous Goods' + dv_charge = 'Declared Value' + ea_charge = 'Extended Area' + freight_charge = 'Freight Charge' + fuel_surcharge = 'Fuel Surcharge' + handling = 'Handling Charge' + premium_charge = 'Premium Service Charge' + ra_charge = 'Residential Address Surcharge' + rural_charge = 'Rural Address Surcharge' + xc_charge = 'Extra Care Charge' + + +class Service(Enum): + canpar_ground = '1' + canpar_usa = '2' + canpar_select_letter = '3' + canpar_select_pak = '4' + canpar_select = '5' + canpar_overnight_letter = 'C' + canpar_overnight_pak = 'D' + canpar_overnight = 'E' + canpar_usa_letter = 'F' + canpar_usa_pak = 'G' + canpar_select_usa = 'H' + canpar_international = 'I' + + +class Option(Flag): + canpar_cash_on_delivery = 'N' + canpar_dangerous_goods = 'dg' + canpar_extra_care = 'xc' + canpar_ten_am = 'A' + canpar_noon = 'B' + canpar_no_signature_required = '2' + canpar_not_no_signature_required = '0' + canpar_saturday = 'S' + + """ Unified Option type mapping """ + cash_on_delivery = canpar_cash_on_delivery diff --git a/extensions/canpar/purplship/providers/canpar/utils.py b/extensions/canpar/purplship/providers/canpar/utils.py new file mode 100644 index 0000000000..df80308155 --- /dev/null +++ b/extensions/canpar/purplship/providers/canpar/utils.py @@ -0,0 +1,41 @@ +"""Purplship Canpar client settings.""" + +from purplship.core.settings import Settings as BaseSettings +from purplship.core.utils import Envelope, apply_namespaceprefix, export + + +class Settings(BaseSettings): + """Canpar connection settings.""" + + user_id: str + password: str + language: str = "en" + id: str = None + + @property + def carrier_name(self): + return "canpar" + + @property + def server_url(self): + return ( + 'https://sandbox.canpar.com/canshipws/services' + if self.test else + 'https://canship.canpar.com/canshipws/services' + ) + + +def default_request_serializer(envelope: Envelope) -> str: + namespace_ = ( + ' xmlns:soap="http://www.w3.org/2003/05/soap-envelope"' + ' xmlns:ws="http://ws.onlinerating.canshipws.canpar.com"' + ' xmlns="http://ws.dto.canshipws.canpar.com/xsd"' + ' xmlns:xsd1="http://dto.canshipws.canpar.com/xsd"' + ) + envelope.ns_prefix_ = 'soapenv' + envelope.Body.ns_prefix_ = envelope.ns_prefix_ + envelope.Body.anytypeobjs_[0].ns_prefix_ = "ws" + apply_namespaceprefix(envelope.Body.anytypeobjs_[0].request, "") + envelope.Body.anytypeobjs_[0].request.ns_prefix_ = "ws" + + return export(envelope, namespacedef_=namespace_) diff --git a/extensions/canpar/setup.py b/extensions/canpar/setup.py new file mode 100644 index 0000000000..9289a1d9e9 --- /dev/null +++ b/extensions/canpar/setup.py @@ -0,0 +1,21 @@ +from setuptools import setup, find_namespace_packages + +setup( + name="purplship.canpar", + version="2020.10.0", + description="Multi-carrier shipping API integration with python", + url="https://github.com/Purplship/purplship", + author="Purplship Team", + license="LGPLv3", + packages=find_namespace_packages(), + install_requires=[ + "purplship", + "py-canpar", + ], + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: OS Independent", + ], + zip_safe=False, +) diff --git a/extensions/dhl_express/purplship/mappers/dhl_express/mapper.py b/extensions/dhl_express/purplship/mappers/dhl_express/mapper.py index 93f6a33443..6e64a08b36 100644 --- a/extensions/dhl_express/purplship/mappers/dhl_express/mapper.py +++ b/extensions/dhl_express/purplship/mappers/dhl_express/mapper.py @@ -18,7 +18,7 @@ PickupRequest, PickupDetails, PickupUpdateRequest, - PickupCancellationRequest, + PickupCancelRequest, Message, ConfirmationDetails, AddressValidationRequest, @@ -69,13 +69,13 @@ def create_pickup_request( ) -> Serializable[BookPURequest]: return book_pickup_request(payload, self.settings) - def create_modify_pickup_request( + def create_pickup_update_request( self, payload: PickupUpdateRequest ) -> Serializable[ModifyPURequest]: return modify_pickup_request(payload, self.settings) def create_cancel_pickup_request( - self, payload: PickupCancellationRequest + self, payload: PickupCancelRequest ) -> Serializable[CancelPURequest]: return cancel_pickup_request(payload, self.settings) @@ -106,7 +106,7 @@ def parse_pickup_response( ) -> Tuple[PickupDetails, List[Message]]: return parse_book_pickup_response(response.deserialize(), self.settings) - def parse_modify_pickup_response( + def parse_pickup_update_response( self, response: Deserializable[str] ) -> Tuple[PickupDetails, List[Message]]: return parse_modify_pickup_response(response.deserialize(), self.settings) diff --git a/extensions/dhl_express/purplship/mappers/dhl_express/proxy.py b/extensions/dhl_express/purplship/mappers/dhl_express/proxy.py index 4355ddceaf..0b81213156 100644 --- a/extensions/dhl_express/purplship/mappers/dhl_express/proxy.py +++ b/extensions/dhl_express/purplship/mappers/dhl_express/proxy.py @@ -47,7 +47,7 @@ def create_shipment( return Deserializable(response, to_xml) - def request_pickup( + def schedule_pickup( self, request: Serializable[BookPURequest] ) -> Deserializable[str]: response = self._send_request(request) diff --git a/extensions/dhl_express/purplship/providers/dhl_express/book_pickup.py b/extensions/dhl_express/purplship/providers/dhl_express/book_pickup.py index 0c33def682..6b414f3491 100644 --- a/extensions/dhl_express/purplship/providers/dhl_express/book_pickup.py +++ b/extensions/dhl_express/purplship/providers/dhl_express/book_pickup.py @@ -108,7 +108,7 @@ def book_pickup_request( ), Pickup=Pickup( Pieces=len(payload.parcels), - PickupDate=payload.date, + PickupDate=payload.pickup_date, ReadyByTime=f"{payload.ready_time}:00", CloseTime=f"{payload.closing_time}:00", SpecialInstructions=[payload.instruction], diff --git a/extensions/dhl_express/purplship/providers/dhl_express/cancel_pickup.py b/extensions/dhl_express/purplship/providers/dhl_express/cancel_pickup.py index 0259c1b01e..b7a3cc4955 100644 --- a/extensions/dhl_express/purplship/providers/dhl_express/cancel_pickup.py +++ b/extensions/dhl_express/purplship/providers/dhl_express/cancel_pickup.py @@ -4,7 +4,7 @@ from purplship.core.utils.helpers import export from purplship.core.utils.serializable import Serializable from purplship.core.models import ( - PickupCancellationRequest, + PickupCancelRequest, Message, ConfirmationDetails, ) @@ -24,6 +24,7 @@ def parse_cancel_pickup_response( carrier_name=settings.carrier_name, carrier_id=settings.carrier_id, success=successful, + operation="Cancel Pickup", ) if successful else None @@ -33,19 +34,21 @@ def parse_cancel_pickup_response( def cancel_pickup_request( - payload: PickupCancellationRequest, settings: Settings + payload: PickupCancelRequest, settings: Settings ) -> Serializable[CancelPURequest]: + request = CancelPURequest( Request=settings.Request( MetaData=MetaData(SoftwareName="XMLPI", SoftwareVersion=1.0) ), schemaVersion=3.0, - RegionCode=CountryRegion[payload.country_code].value - if payload.country_code - else "AM", + RegionCode=( + CountryRegion[payload.address.country_code].value + if payload.address is not None and payload.address.country_code is not None else "AM" + ), ConfirmationNumber=payload.confirmation_number, - RequestorName=payload.person_name, - CountryCode=payload.country_code, + RequestorName=payload.address.person_name, + CountryCode=payload.address.country_code, Reason="006", PickupDate=payload.pickup_date, CancelTime=time.strftime("%H:%M:%S"), diff --git a/extensions/dhl_express/purplship/providers/dhl_express/modify_pickup.py b/extensions/dhl_express/purplship/providers/dhl_express/modify_pickup.py index 9083e4bcd8..97399372a4 100644 --- a/extensions/dhl_express/purplship/providers/dhl_express/modify_pickup.py +++ b/extensions/dhl_express/purplship/providers/dhl_express/modify_pickup.py @@ -115,7 +115,7 @@ def modify_pickup_request( ), Pickup=Pickup( Pieces=len(payload.parcels), - PickupDate=payload.date, + PickupDate=payload.pickup_date, ReadyByTime=f"{payload.ready_time}:00", CloseTime=f"{payload.closing_time}:00", SpecialInstructions=[payload.instruction], diff --git a/extensions/dhl_express/purplship/providers/dhl_express/ship_val.py b/extensions/dhl_express/purplship/providers/dhl_express/ship_val.py index ee7d4735d7..ddb60e91c0 100644 --- a/extensions/dhl_express/purplship/providers/dhl_express/ship_val.py +++ b/extensions/dhl_express/purplship/providers/dhl_express/ship_val.py @@ -67,6 +67,7 @@ def _extract_shipment(shipment_node, settings: Settings) -> Optional[ShipmentDet carrier_name=settings.carrier_name, carrier_id=settings.carrier_id, tracking_number=shipment.AirwayBillNumber, + shipment_identifier=shipment.AirwayBillNumber, label=label, ) @@ -214,7 +215,7 @@ def shipment_request( Dutiable=Dutiable( DeclaredCurrency=payload.customs.duty.currency or "USD", DeclaredValue=payload.customs.duty.amount, - TermsOfTrade=payload.customs.terms_of_trade, + TermsOfTrade=payload.customs.incoterm, ) if payload.customs is not None and payload.customs.duty is not None else None, diff --git a/extensions/dhl_express/setup.py b/extensions/dhl_express/setup.py index 814888a2b0..7c0500b5ee 100644 --- a/extensions/dhl_express/setup.py +++ b/extensions/dhl_express/setup.py @@ -3,7 +3,7 @@ setup( name="purplship.dhl_express", - version="2020.9.0", + version="2020.10.0", description="Multi-carrier shipping API integration with python", url="https://github.com/PurplShip/purplship", author="Purplship Team", diff --git a/extensions/fedex/purplship/providers/fedex/address_validation_service.py b/extensions/fedex/purplship/providers/fedex/address_validation_service.py index e9c0b69f39..94dd109ea7 100644 --- a/extensions/fedex/purplship/providers/fedex/address_validation_service.py +++ b/extensions/fedex/purplship/providers/fedex/address_validation_service.py @@ -55,7 +55,7 @@ def address_validation_request(payload: AddressValidationRequest, settings: Sett body_content=FedexAddressValidationRequest( WebAuthenticationDetail=settings.webAuthenticationDetail, ClientDetail=settings.clientDetail, - TransactionDetail=TransactionDetail(CustomerTransactionId="AddressValidationRequest_v2"), + TransactionDetail=TransactionDetail(CustomerTransactionId="AddressValidationRequest_v4"), Version=VersionId(ServiceId="aval", Major=4, Intermediate=0, Minor=0), InEffectAsOfTimestamp=None, AddressesToValidate=[ diff --git a/extensions/fedex/purplship/providers/fedex/freight/ship_service.py b/extensions/fedex/purplship/providers/fedex/freight/ship_service.py index ac63e0ca16..b1e606fed5 100644 --- a/extensions/fedex/purplship/providers/fedex/freight/ship_service.py +++ b/extensions/fedex/purplship/providers/fedex/freight/ship_service.py @@ -97,6 +97,7 @@ def _extract_shipment( carrier_name=settings.carrier_name, carrier_id=settings.carrier_id, tracking_number=tracking_number, + shipment_identifier=tracking_number, label=label, ) diff --git a/extensions/fedex/purplship/providers/fedex/package/__init__.py b/extensions/fedex/purplship/providers/fedex/package/__init__.py index 318d69781a..7a44cd3cd4 100644 --- a/extensions/fedex/purplship/providers/fedex/package/__init__.py +++ b/extensions/fedex/purplship/providers/fedex/package/__init__.py @@ -1,2 +1,3 @@ -from .rate_service import rate_request, parse_rate_response -from .ship_service import process_shipment_request, parse_shipment_response +from purplship.providers.fedex.package.rate_service import rate_request, parse_rate_response +from purplship.providers.fedex.package.ship_service import process_shipment_request, parse_shipment_response +from purplship.providers.fedex.package.void_service import void_shipment_request, parse_void_shipment_response diff --git a/extensions/fedex/purplship/providers/fedex/package/rate_service.py b/extensions/fedex/purplship/providers/fedex/package/rate_service.py index c01c59a095..e172451420 100644 --- a/extensions/fedex/purplship/providers/fedex/package/rate_service.py +++ b/extensions/fedex/purplship/providers/fedex/package/rate_service.py @@ -17,7 +17,6 @@ RequestedPackageLineItem, Dimensions, WeightUnits, - DistanceUnits, ) from purplship.core.utils import export, concat_str, Serializable, decimal, to_date from purplship.core.utils.soap import create_envelope, apply_namespaceprefix diff --git a/extensions/fedex/purplship/providers/fedex/package/ship_service.py b/extensions/fedex/purplship/providers/fedex/package/ship_service.py index ac63e0ca16..b1e606fed5 100644 --- a/extensions/fedex/purplship/providers/fedex/package/ship_service.py +++ b/extensions/fedex/purplship/providers/fedex/package/ship_service.py @@ -97,6 +97,7 @@ def _extract_shipment( carrier_name=settings.carrier_name, carrier_id=settings.carrier_id, tracking_number=tracking_number, + shipment_identifier=tracking_number, label=label, ) diff --git a/extensions/fedex/purplship/providers/fedex/package/void_service.py b/extensions/fedex/purplship/providers/fedex/package/void_service.py new file mode 100644 index 0000000000..a650454d65 --- /dev/null +++ b/extensions/fedex/purplship/providers/fedex/package/void_service.py @@ -0,0 +1,67 @@ +from typing import List, Tuple +from pyfedex.ship_service_v25 import ( + DeleteShipmentRequest, + TrackingId, + TransactionDetail, + VersionId, + DeletionControlType, + TrackingIdType, +) +from purplship.core.models import ( + ShipmentCancelRequest, + ConfirmationDetails, + Message +) +from purplship.core.utils import ( + Envelope, + Element, + Serializable, + create_envelope, +) +from purplship.providers.fedex.error import parse_error_response +from purplship.providers.fedex.utils import Settings, default_request_serializer + + +def parse_void_shipment_response(response: Element, settings: Settings) -> Tuple[ConfirmationDetails, List[Message]]: + errors = parse_error_response(response, settings) + success = len(errors) == 0 + confirmation: ConfirmationDetails = ConfirmationDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + success=success, + operation="Cancel Shipment", + ) if success else None + + return confirmation, errors + + +def void_shipment_request(payload: ShipmentCancelRequest, settings: Settings) -> Serializable[Envelope]: + tracking_type = next( + (t for t in list(TrackingIdType) if t.name.lower() in payload.service), + TrackingIdType.FEDEX + ).value + deletion_type = DeletionControlType[ + payload.options.get('deletion_type', 'DELETE_ALL_PACKAGES') + ].value + + request = create_envelope( + body_content=DeleteShipmentRequest( + WebAuthenticationDetail=settings.webAuthenticationDetail, + ClientDetail=settings.clientDetail, + TransactionDetail=TransactionDetail(CustomerTransactionId="Delete Shipment"), + Version=VersionId(ServiceId="ship", Major=23, Intermediate=0, Minor=0), + ShipTimestamp=None, + TrackingId=TrackingId( + TrackingIdType=tracking_type, + FormId=None, + UspsApplicationId=None, + TrackingNumber=payload.shipment_identifier + ), + DeletionControl=deletion_type + ) + ) + + return Serializable( + request, + default_request_serializer('v23', 'xmlns:v23="http://fedex.com/ws/ship/v23"') + ) diff --git a/extensions/fedex/purplship/providers/fedex/pickup/__init__.py b/extensions/fedex/purplship/providers/fedex/pickup/__init__.py index bc4c854927..c0d612b1f8 100644 --- a/extensions/fedex/purplship/providers/fedex/pickup/__init__.py +++ b/extensions/fedex/purplship/providers/fedex/pickup/__init__.py @@ -9,7 +9,7 @@ from purplship.core.models import ( PickupRequest, PickupUpdateRequest, - PickupCancellationRequest, + PickupCancelRequest, ) from purplship.providers.fedex.utils import Settings from purplship.providers.fedex.pickup.request import ( @@ -98,7 +98,7 @@ def _cancel_pickup_request( new_pickup = build(CreatePickupReply, reply) data = ( cancel_pickup_request( - PickupCancellationRequest(confirmation_number=payload.confirmation_number), + PickupCancelRequest(confirmation_number=payload.confirmation_number), settings, ) if new_pickup is not None diff --git a/extensions/fedex/purplship/providers/fedex/pickup/availability.py b/extensions/fedex/purplship/providers/fedex/pickup/availability.py index 1d827a32c3..7de18b34e4 100644 --- a/extensions/fedex/purplship/providers/fedex/pickup/availability.py +++ b/extensions/fedex/purplship/providers/fedex/pickup/availability.py @@ -25,7 +25,7 @@ def pickup_availability_request( payload: PickupRequest, settings: Settings ) -> Serializable[PickupAvailabilityRequest]: - same_day = to_date(payload.date).date() == datetime.today().date() + same_day = to_date(payload.pickup_date).date() == datetime.today().date() request = PickupAvailabilityRequest( WebAuthenticationDetail=settings.webAuthenticationDetail, @@ -52,7 +52,7 @@ def pickup_availability_request( PickupRequestType.SAME_DAY if same_day else PickupRequestType.FUTURE_DAY ).value ], - DispatchDate=payload.date, + DispatchDate=payload.pickup_date, NumberOfBusinessDays=None, PackageReadyTime=f"{payload.ready_time}:00", CustomerCloseTime=f"{payload.closing_time}:00", diff --git a/extensions/fedex/purplship/providers/fedex/pickup/cancel.py b/extensions/fedex/purplship/providers/fedex/pickup/cancel.py index 8a165a0601..30621535c0 100644 --- a/extensions/fedex/purplship/providers/fedex/pickup/cancel.py +++ b/extensions/fedex/purplship/providers/fedex/pickup/cancel.py @@ -8,7 +8,7 @@ NotificationSeverityType, ) from purplship.core.models import ( - PickupCancellationRequest, + PickupCancelRequest, ConfirmationDetails, Message, ) @@ -41,13 +41,14 @@ def parse_cancel_pickup_reply( carrier_id=settings.carrier_id, carrier_name=settings.carrier_name, success=reply.HighestSeverity == NotificationSeverityType.SUCCESS.value, + operation="Cancel Pickup", ) return cancellation, parse_error_response(response, settings) def cancel_pickup_request( - payload: PickupCancellationRequest, settings: Settings + payload: PickupCancelRequest, settings: Settings ) -> Serializable[CancelPickupRequest]: request = CancelPickupRequest( diff --git a/extensions/fedex/purplship/providers/fedex/pickup/request.py b/extensions/fedex/purplship/providers/fedex/pickup/request.py index a39f713f9c..cf99d7eede 100644 --- a/extensions/fedex/purplship/providers/fedex/pickup/request.py +++ b/extensions/fedex/purplship/providers/fedex/pickup/request.py @@ -68,7 +68,7 @@ def _extract_pickup_details( def pickup_request( payload: PickupRequest, settings: Settings ) -> Serializable[CreatePickupRequest]: - same_day = to_date(payload.date).date() == datetime.today().date() + same_day = to_date(payload.pickup_date).date() == datetime.today().date() packages = Packages(payload.parcels, PackagePresets, required=["weight"]) request = CreatePickupRequest( @@ -103,7 +103,7 @@ def pickup_request( ), ), PackageLocation=payload.package_location, - ReadyTimestamp=f"{payload.date}T{payload.ready_time}:00", + ReadyTimestamp=f"{payload.pickup_date}T{payload.ready_time}:00", CompanyCloseTime=f"{payload.closing_time}:00", PickupDateType=( PickupRequestType.SAME_DAY if same_day else PickupRequestType.FUTURE_DAY diff --git a/extensions/fedex/setup.py b/extensions/fedex/setup.py index 5c95e498bd..6e561e71ce 100644 --- a/extensions/fedex/setup.py +++ b/extensions/fedex/setup.py @@ -3,7 +3,7 @@ setup( name="purplship.fedex", - version="2020.9.0", + version="2020.10.0", description="Multi-carrier shipping API integration with python", url="https://github.com/PurplShip/purplship", author="Purplship Team", diff --git a/extensions/fedex_express/purplship/mappers/fedex_express/mapper.py b/extensions/fedex_express/purplship/mappers/fedex_express/mapper.py index 47a702a04e..b14bf3ac02 100644 --- a/extensions/fedex_express/purplship/mappers/fedex_express/mapper.py +++ b/extensions/fedex_express/purplship/mappers/fedex_express/mapper.py @@ -15,10 +15,11 @@ PickupDetails, PickupRequest, PickupUpdateRequest, - PickupCancellationRequest, + PickupCancelRequest, ConfirmationDetails, AddressValidationRequest, - AddressValidationDetails + AddressValidationDetails, + ShipmentCancelRequest, ) from purplship.providers.fedex import ( track_request, @@ -38,6 +39,8 @@ parse_rate_response, process_shipment_request, parse_shipment_response, + void_shipment_request, + parse_void_shipment_response, ) from purplship.mappers.fedex_express.settings import Settings from purplship.api.mapper import Mapper as BaseMapper @@ -66,16 +69,19 @@ def create_shipment_request( ) -> Serializable[ProcessShipmentRequest]: return process_shipment_request(payload, self.settings) + def create_cancel_shipment_request(self, payload: ShipmentCancelRequest) -> Serializable: + return void_shipment_request(payload, self.settings) + def create_pickup_request(self, payload: PickupRequest) -> Serializable[Pipeline]: return create_pickup_request(payload, self.settings) - def create_modify_pickup_request( + def create_pickup_update_request( self, payload: PickupUpdateRequest ) -> Serializable[Pipeline]: return update_pickup_request(payload, self.settings) def create_cancel_pickup_request( - self, payload: PickupCancellationRequest + self, payload: PickupCancelRequest ) -> Serializable[CancelPickupRequest]: return cancel_pickup_request(payload, self.settings) @@ -101,12 +107,17 @@ def parse_shipment_response( ) -> Tuple[ShipmentDetails, List[Message]]: return parse_shipment_response(response.deserialize(), self.settings) + def parse_cancel_shipment_response( + self, response: Deserializable[str] + ) -> Tuple[ConfirmationDetails, List[Message]]: + return parse_void_shipment_response(response.deserialize(), self.settings) + def parse_pickup_response( self, response: Deserializable[str] ) -> Tuple[PickupDetails, List[Message]]: return parse_pickup_response(response.deserialize(), self.settings) - def parse_modify_pickup_response( + def parse_pickup_update_response( self, response: Deserializable[str] ) -> Tuple[PickupDetails, List[Message]]: return parse_pickup_response(response.deserialize(), self.settings) diff --git a/extensions/fedex_express/purplship/mappers/fedex_express/proxy.py b/extensions/fedex_express/purplship/mappers/fedex_express/proxy.py index 5c0e1da9de..fb17300be2 100644 --- a/extensions/fedex_express/purplship/mappers/fedex_express/proxy.py +++ b/extensions/fedex_express/purplship/mappers/fedex_express/proxy.py @@ -46,7 +46,12 @@ def create_shipment( return Deserializable(response, to_xml) - def request_pickup(self, request: Serializable[Pipeline]) -> Deserializable[str]: + def cancel_shipment(self, request: Serializable[Envelope]) -> Deserializable[str]: + response = self._send_request("/ship", request) + + return Deserializable(response, to_xml) + + def schedule_pickup(self, request: Serializable[Pipeline]) -> Deserializable[str]: def process(job: Job): if job.data is None: return job.fallback diff --git a/extensions/fedex_express/setup.py b/extensions/fedex_express/setup.py index bc7a2a558e..17a584f3a0 100644 --- a/extensions/fedex_express/setup.py +++ b/extensions/fedex_express/setup.py @@ -3,7 +3,7 @@ setup( name="purplship.fedex_express", - version="2020.9.0", + version="2020.10.0", description="Multi-carrier shipping API integration with python", url="https://github.com/PurplShip/purplship", author="Purplship Team", diff --git a/extensions/fedex_freight/setup.py b/extensions/fedex_freight/setup.py index 1dcdacf23b..358ff0090c 100644 --- a/extensions/fedex_freight/setup.py +++ b/extensions/fedex_freight/setup.py @@ -3,7 +3,7 @@ setup( name="purplship.fedex_freight", - version="2020.9.0", + version="2020.10.0", description="Multi-carrier shipping API integration with python", url="https://github.com/PurplShip/purplship", author="Purplship Team", diff --git a/extensions/purolator/purplship/providers/purolator/package/__init__.py b/extensions/purolator/purplship/providers/purolator/package/__init__.py index b8f756307c..e94a79c1d5 100644 --- a/extensions/purolator/purplship/providers/purolator/package/__init__.py +++ b/extensions/purolator/purplship/providers/purolator/package/__init__.py @@ -1,6 +1,8 @@ from purplship.providers.purolator.package.shipping_service import ( create_shipment_request, parse_shipment_creation_response, + parse_void_shipment_response, + void_shipment_request, ) from purplship.providers.purolator.package.estimate_service import ( get_full_estimate_request, diff --git a/extensions/purolator/purplship/providers/purolator/package/pickup_service/schedule_pickup.py b/extensions/purolator/purplship/providers/purolator/package/pickup_service/schedule_pickup.py index ded26856b5..c66f7a7526 100644 --- a/extensions/purolator/purplship/providers/purolator/package/pickup_service/schedule_pickup.py +++ b/extensions/purolator/purplship/providers/purolator/package/pickup_service/schedule_pickup.py @@ -88,7 +88,7 @@ def schedule_pickup_request( BillingAccountNumber=settings.account_number, PartnerID=None, PickupInstruction=PickupInstruction( - Date=payload.date, + Date=payload.pickup_date, AnyTimeAfter="".join(payload.ready_time.split(":")), UntilTime="".join(payload.closing_time.split(":")), TotalWeight=Weight( diff --git a/extensions/purolator/purplship/providers/purolator/package/pickup_service/validate_pickup.py b/extensions/purolator/purplship/providers/purolator/package/pickup_service/validate_pickup.py index 1efb4f4779..9c81b03446 100644 --- a/extensions/purolator/purplship/providers/purolator/package/pickup_service/validate_pickup.py +++ b/extensions/purolator/purplship/providers/purolator/package/pickup_service/validate_pickup.py @@ -45,7 +45,7 @@ def validate_pickup_request( BillingAccountNumber=settings.account_number, PartnerID=None, PickupInstruction=PickupInstruction( - Date=payload.date, + Date=payload.pickup_date, AnyTimeAfter="".join(payload.ready_time.split(":")), UntilTime="".join(payload.closing_time.split(":")), TotalWeight=Weight( diff --git a/extensions/purolator/purplship/providers/purolator/package/pickup_service/void_pickup.py b/extensions/purolator/purplship/providers/purolator/package/pickup_service/void_pickup.py index 4e1ddb7815..f521a21712 100644 --- a/extensions/purolator/purplship/providers/purolator/package/pickup_service/void_pickup.py +++ b/extensions/purolator/purplship/providers/purolator/package/pickup_service/void_pickup.py @@ -5,7 +5,7 @@ RequestContext, ) from purplship.core.models import ( - PickupCancellationRequest, + PickupCancelRequest, ConfirmationDetails, Message, ) @@ -23,6 +23,7 @@ def parse_void_pickup_reply( carrier_id=settings.carrier_id, carrier_name=settings.carrier_name, success=True, + operation="Cancel Pickup", ) if not any(errors) else None @@ -32,7 +33,7 @@ def parse_void_pickup_reply( def void_pickup_request( - payload: PickupCancellationRequest, settings: Settings + payload: PickupCancelRequest, settings: Settings ) -> Serializable[Envelope]: request = create_envelope( diff --git a/extensions/purolator/purplship/providers/purolator/package/shipping_service/__init__.py b/extensions/purolator/purplship/providers/purolator/package/shipping_service/__init__.py index a55b5552d7..cf18c81089 100644 --- a/extensions/purolator/purplship/providers/purolator/package/shipping_service/__init__.py +++ b/extensions/purolator/purplship/providers/purolator/package/shipping_service/__init__.py @@ -18,6 +18,10 @@ from purplship.providers.purolator.package.shipping_service.create_shipping import ( create_shipping_request, ) +from purplship.providers.purolator.package.shipping_service.void_shipment import ( + parse_void_shipment_response, + void_shipment_request, +) ShipmentRequestType = Type[Union[ValidateShipmentRequest, CreateShipmentRequest]] @@ -50,11 +54,13 @@ def _extract_shipment(response: Element, settings: Settings) -> ShipmentDetails: (content for content in [document.Data, document.URL] if content is not None), "No label returned", ) + pin = cast(PIN, shipment.ShipmentPIN).Value return ShipmentDetails( carrier_name=settings.carrier_name, carrier_id=settings.carrier_id, - tracking_number=cast(PIN, shipment.ShipmentPIN).Value, + tracking_number=pin, + shipment_identifier=pin, label=label, ) diff --git a/extensions/purolator/purplship/providers/purolator/package/shipping_service/void_shipment.py b/extensions/purolator/purplship/providers/purolator/package/shipping_service/void_shipment.py new file mode 100644 index 0000000000..37ff5bc4c1 --- /dev/null +++ b/extensions/purolator/purplship/providers/purolator/package/shipping_service/void_shipment.py @@ -0,0 +1,54 @@ +from typing import Tuple, List +from functools import partial +from pypurolator.shipping_service_2_1_3 import ( + VoidShipmentRequest, + VoidShipmentResponse, + RequestContext, + PIN, +) +from purplship.core.models import ( + ShipmentCancelRequest, + ConfirmationDetails, + Message, +) +from purplship.core.utils import Serializable, create_envelope, Envelope, Element, build +from purplship.providers.purolator.error import parse_error_response +from purplship.providers.purolator.utils import Settings, standard_request_serializer + + +def parse_void_shipment_response( + response: Element, settings: Settings +) -> Tuple[ConfirmationDetails, List[Message]]: + void_response = build(VoidShipmentResponse, next( + iter(response.xpath(".//*[local-name() = $name]", name="VoidShipmentResponse")), + None + )) + voided = void_response is not None and void_response.ShipmentVoided + cancellation = ConfirmationDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + success=True, + operation="Cancel Shipment", + ) if voided else None + + return cancellation, parse_error_response(response, settings) + + +def void_shipment_request( + payload: ShipmentCancelRequest, settings: Settings +) -> Serializable[Envelope]: + + request = create_envelope( + header_content=RequestContext( + Version="1.0", + Language=settings.language, + GroupID="", + RequestReference="", + UserToken=settings.user_token, + ), + body_content=VoidShipmentRequest( + PIN=PIN(Value=payload.shipment_identifier) + ), + ) + + return Serializable(request, partial(standard_request_serializer, version="v1")) diff --git a/extensions/purolator/setup.py b/extensions/purolator/setup.py index 50603ffb33..7f9106fd43 100644 --- a/extensions/purolator/setup.py +++ b/extensions/purolator/setup.py @@ -3,7 +3,7 @@ setup( name="purplship.purolator", - version="2020.9.0", + version="2020.10.0", description="Multi-carrier shipping API integration with python", url="https://github.com/PurplShip/purplship", author="Purplship Team", diff --git a/extensions/purolator_courier/purplship/mappers/purolator_courier/mapper.py b/extensions/purolator_courier/purplship/mappers/purolator_courier/mapper.py index e28cd066d3..07e16a0d40 100644 --- a/extensions/purolator_courier/purplship/mappers/purolator_courier/mapper.py +++ b/extensions/purolator_courier/purplship/mappers/purolator_courier/mapper.py @@ -15,9 +15,10 @@ PickupUpdateRequest, PickupDetails, ConfirmationDetails, - PickupCancellationRequest, + PickupCancelRequest, AddressValidationDetails, AddressValidationRequest, + ShipmentCancelRequest, ) from purplship.providers.purolator.package import ( parse_track_package_response, @@ -34,6 +35,8 @@ parse_modify_pickup_reply, validate_address_request, parse_validate_address_response, + parse_void_shipment_response, + void_shipment_request, ) @@ -58,16 +61,19 @@ def create_shipment_request( ) -> Serializable[Pipeline]: return create_shipment_request(payload, self.settings) + def create_cancel_shipment_request(self, payload: ShipmentCancelRequest) -> Serializable: + return void_shipment_request(payload, self.settings) + def create_pickup_request(self, payload: PickupRequest) -> Serializable[Pipeline]: return schedule_pickup_pipeline(payload, self.settings) - def create_modify_pickup_request( + def create_pickup_update_request( self, payload: PickupUpdateRequest ) -> Serializable[Pipeline]: return modify_pickup_pipeline(payload, self.settings) def create_cancel_pickup_request( - self, payload: PickupCancellationRequest + self, payload: PickupCancelRequest ) -> Serializable[Envelope]: return void_pickup_request(payload, self.settings) @@ -93,12 +99,17 @@ def parse_shipment_response( ) -> Tuple[ShipmentDetails, List[Message]]: return parse_shipment_creation_response(response.deserialize(), self.settings) + def parse_cancel_shipment_response( + self, response: Deserializable[str] + ) -> Tuple[ConfirmationDetails, List[Message]]: + return parse_void_shipment_response(response.deserialize(), self.settings) + def parse_pickup_response( self, response: Deserializable[str] ) -> Tuple[PickupDetails, List[Message]]: return parse_schedule_pickup_reply(response.deserialize(), self.settings) - def parse_modify_pickup_response( + def parse_pickup_update_response( self, response: Deserializable[str] ) -> Tuple[PickupDetails, List[Message]]: return parse_modify_pickup_reply(response.deserialize(), self.settings) diff --git a/extensions/purolator_courier/purplship/mappers/purolator_courier/proxy.py b/extensions/purolator_courier/purplship/mappers/purolator_courier/proxy.py index ca5af6474b..9cde323184 100644 --- a/extensions/purolator_courier/purplship/mappers/purolator_courier/proxy.py +++ b/extensions/purolator_courier/purplship/mappers/purolator_courier/proxy.py @@ -76,7 +76,16 @@ def process(job: Job): _, *response = pipeline.apply(process) return Deserializable(bundle_xml(response), to_xml) - def request_pickup(self, request: Serializable[Pipeline]) -> Deserializable[str]: + def cancel_shipment(self, request: Serializable) -> Deserializable: + response = self._send_request( + path="/EWS/V2/Shipping/ShippingService.asmx", + soapaction="http://purolator.com/pws/service/v2/VoidShipment", + request=request, + ) + + return Deserializable(response, to_xml) + + def schedule_pickup(self, request: Serializable[Pipeline]) -> Deserializable[str]: def process(job: Job): if job.data is None: return job.fallback diff --git a/extensions/purolator_courier/setup.py b/extensions/purolator_courier/setup.py index 9f49d4793b..da1a0d01a8 100644 --- a/extensions/purolator_courier/setup.py +++ b/extensions/purolator_courier/setup.py @@ -3,7 +3,7 @@ setup( name="purplship.purolator_courier", - version="2020.9.0", + version="2020.10.0", description="Multi-carrier shipping API integration with python", url="https://github.com/PurplShip/purplship", author="Purplship Team", diff --git a/extensions/ups/purplship/providers/ups/freight/ship.py b/extensions/ups/purplship/providers/ups/freight/ship.py index 65c555e776..fcaae09c50 100644 --- a/extensions/ups/purplship/providers/ups/freight/ship.py +++ b/extensions/ups/purplship/providers/ups/freight/ship.py @@ -67,6 +67,7 @@ def _extract_shipment(shipment_node: Element, settings: Settings) -> ShipmentDet carrier_name=settings.carrier_name, carrier_id=settings.carrier_id, tracking_number=shipment.ShipmentNumber, + shipment_identifier=shipment.ShipmentNumber, label=label, ) diff --git a/extensions/ups/purplship/providers/ups/package/__init__.py b/extensions/ups/purplship/providers/ups/package/__init__.py index b00d39d9ac..a5ebe4c50a 100644 --- a/extensions/ups/purplship/providers/ups/package/__init__.py +++ b/extensions/ups/purplship/providers/ups/package/__init__.py @@ -1,6 +1,8 @@ from purplship.providers.ups.package.ship import ( shipment_request, parse_shipment_response, + void_shipment_request, + parse_void_shipment_response, ) from purplship.providers.ups.package.rate import rate_request, parse_rate_response from purplship.providers.ups.package.pickup import ( diff --git a/extensions/ups/purplship/providers/ups/package/pickup/__init__.py b/extensions/ups/purplship/providers/ups/package/pickup/__init__.py index 14d9fa63fc..76a48d6f45 100644 --- a/extensions/ups/purplship/providers/ups/package/pickup/__init__.py +++ b/extensions/ups/purplship/providers/ups/package/pickup/__init__.py @@ -5,7 +5,7 @@ from purplship.core.models import ( PickupRequest, PickupUpdateRequest, - PickupCancellationRequest, + PickupCancelRequest, ) from purplship.providers.ups.utils import Settings from purplship.providers.ups.package.pickup.create import ( @@ -88,7 +88,7 @@ def _cancel_pickup_request( new_pickup = build(PickupCreationResponse, reply) data = ( cancel_pickup_request( - PickupCancellationRequest(confirmation_number=payload.confirmation_number), + PickupCancelRequest(confirmation_number=payload.confirmation_number), settings, ) if new_pickup is not None and new_pickup.PRN is not None diff --git a/extensions/ups/purplship/providers/ups/package/pickup/cancel.py b/extensions/ups/purplship/providers/ups/package/pickup/cancel.py index e955ca9464..0b2ffc9816 100644 --- a/extensions/ups/purplship/providers/ups/package/pickup/cancel.py +++ b/extensions/ups/purplship/providers/ups/package/pickup/cancel.py @@ -1,12 +1,12 @@ from typing import List, Tuple from pyups.pickup_web_service_schema import ( - PickupCancelRequest, + PickupCancelRequest as UPSPickupCancelRequest, CodeDescriptionType, RequestType, ) from purplship.core.utils import Envelope, Element, create_envelope, Serializable, build from purplship.core.models import ( - PickupCancellationRequest, + PickupCancelRequest, ConfirmationDetails, Message, ) @@ -30,6 +30,7 @@ def parse_cancel_pickup_response( carrier_id=settings.carrier_id, carrier_name=settings.carrier_name, success=success, + operation="Cancel Pickup", ) if success else None @@ -39,13 +40,15 @@ def parse_cancel_pickup_response( def cancel_pickup_request( - payload: PickupCancellationRequest, settings: Settings + payload: PickupCancelRequest, settings: Settings ) -> Serializable[Envelope]: request = create_envelope( header_content=settings.Security, - body_content=PickupCancelRequest( - Request=RequestType(), CancelBy="02", PRN=payload.confirmation_number + body_content=UPSPickupCancelRequest( + Request=RequestType(), + CancelBy="02", + PRN=payload.confirmation_number ), ) diff --git a/extensions/ups/purplship/providers/ups/package/pickup/create.py b/extensions/ups/purplship/providers/ups/package/pickup/create.py index 1cc6056b4b..063e652d29 100644 --- a/extensions/ups/purplship/providers/ups/package/pickup/create.py +++ b/extensions/ups/purplship/providers/ups/package/pickup/create.py @@ -115,7 +115,7 @@ def create_pickup_request( PickupDateInfo=PickupDateInfoType( CloseTime=format_time(payload.closing_time, "%H:%M", "%H%M"), ReadyTime=format_time(payload.ready_time, "%H:%M", "%H%M"), - PickupDate=to_date(payload.date).strftime("%Y%m%d"), + PickupDate=to_date(payload.pickup_date).strftime("%Y%m%d"), ), PickupAddress=PickupAddressType( CompanyName=payload.address.company_name, diff --git a/extensions/ups/purplship/providers/ups/package/pickup/rate.py b/extensions/ups/purplship/providers/ups/package/pickup/rate.py index d38a387ac4..e0eec23a1e 100644 --- a/extensions/ups/purplship/providers/ups/package/pickup/rate.py +++ b/extensions/ups/purplship/providers/ups/package/pickup/rate.py @@ -21,7 +21,7 @@ def pickup_rate_request( payload: PickupRequest, settings: Settings ) -> Serializable[Envelope]: - pickup_date = to_date(payload.date) + pickup_date = to_date(payload.pickup_date) same_day = pickup_date.date() == datetime.today().date() request = create_envelope( diff --git a/extensions/ups/purplship/providers/ups/package/ship/__init__.py b/extensions/ups/purplship/providers/ups/package/ship/__init__.py new file mode 100644 index 0000000000..1b454e7100 --- /dev/null +++ b/extensions/ups/purplship/providers/ups/package/ship/__init__.py @@ -0,0 +1,8 @@ +from purplship.providers.ups.package.ship.create import ( + shipment_request, + parse_shipment_response, +) +from purplship.providers.ups.package.ship.void import ( + void_shipment_request, + parse_void_shipment_response, +) diff --git a/extensions/ups/purplship/providers/ups/package/ship.py b/extensions/ups/purplship/providers/ups/package/ship/create.py similarity index 99% rename from extensions/ups/purplship/providers/ups/package/ship.py rename to extensions/ups/purplship/providers/ups/package/ship/create.py index e988d21eab..9da201caa3 100644 --- a/extensions/ups/purplship/providers/ups/package/ship.py +++ b/extensions/ups/purplship/providers/ups/package/ship/create.py @@ -69,6 +69,7 @@ def _extract_shipment(node: Element, settings: Settings) -> ShipmentDetails: carrier_name=settings.carrier_name, carrier_id=settings.carrier_id, tracking_number=shipment.ShipmentIdentificationNumber, + shipment_identifier=shipment.ShipmentIdentificationNumber, label=gif_to_pdf(graphic), ) diff --git a/extensions/ups/purplship/providers/ups/package/ship/void.py b/extensions/ups/purplship/providers/ups/package/ship/void.py new file mode 100644 index 0000000000..61ccee3f31 --- /dev/null +++ b/extensions/ups/purplship/providers/ups/package/ship/void.py @@ -0,0 +1,63 @@ +from typing import List, Tuple +from pyups.void_web_service_schema import ( + VoidShipmentRequest, + CodeDescriptionType, + RequestType, + VoidShipmentType, +) +from purplship.core.utils import Envelope, Element, create_envelope, Serializable, build +from purplship.core.models import ( + ShipmentCancelRequest, + ConfirmationDetails, + Message, +) +from purplship.providers.ups.utils import Settings, default_request_serializer +from purplship.providers.ups.error import parse_error_response + + +def parse_void_shipment_response( + response: Element, settings: Settings +) -> Tuple[ConfirmationDetails, List[Message]]: + status = build( + CodeDescriptionType, + next( + iter(response.xpath(".//*[local-name() = $name]", name="ResponseStatus")), + None, + ), + ) + success = status is not None and status.Code == "1" + cancellation = ( + ConfirmationDetails( + carrier_id=settings.carrier_id, + carrier_name=settings.carrier_name, + success=success, + operation="Cancel Shipment", + ) + if success + else None + ) + + return cancellation, parse_error_response(response, settings) + + +def void_shipment_request( + payload: ShipmentCancelRequest, settings: Settings +) -> Serializable[Envelope]: + + request = create_envelope( + header_content=settings.Security, + body_content=VoidShipmentRequest( + Request=RequestType(), + VoidShipment=VoidShipmentType( + ShipmentIdentificationNumber=payload.shipment_identifier, + TrackingNumber=None + ) + ), + ) + + return Serializable( + request, + default_request_serializer( + "void", 'xmlns:void="http://www.ups.com/XMLSchema/XOLTWS/Ship/v1.0"' + ), + ) diff --git a/extensions/ups/setup.py b/extensions/ups/setup.py index 56c55c6d33..add36def43 100644 --- a/extensions/ups/setup.py +++ b/extensions/ups/setup.py @@ -3,7 +3,7 @@ setup( name="purplship.ups", - version="2020.9.0", + version="2020.10.0", description="Multi-carrier shipping API integration with python", url="https://github.com/PurplShip/purplship", author="Purplship Team", diff --git a/extensions/ups_freight/setup.py b/extensions/ups_freight/setup.py index 15123c1bee..7b08e55225 100644 --- a/extensions/ups_freight/setup.py +++ b/extensions/ups_freight/setup.py @@ -3,7 +3,7 @@ setup( name="purplship.ups_freight", - version="2020.9.0", + version="2020.10.0", description="Multi-carrier shipping API integration with python", url="https://github.com/PurplShip/purplship", author="Purplship Team", diff --git a/extensions/ups_package/purplship/mappers/ups_package/mapper.py b/extensions/ups_package/purplship/mappers/ups_package/mapper.py index 296d6d11ee..ccf23f7c76 100644 --- a/extensions/ups_package/purplship/mappers/ups_package/mapper.py +++ b/extensions/ups_package/purplship/mappers/ups_package/mapper.py @@ -13,11 +13,12 @@ Message, PickupRequest, PickupUpdateRequest, - PickupCancellationRequest, + PickupCancelRequest, PickupDetails, ConfirmationDetails, AddressValidationRequest, AddressValidationDetails, + ShipmentCancelRequest, ) from purplship.providers.ups import ( parse_track_response, @@ -35,6 +36,8 @@ update_pickup_pipeline, cancel_pickup_request, parse_cancel_pickup_response, + void_shipment_request, + parse_void_shipment_response, ) from pyups.track_web_service_schema import TrackRequest from pyups.ship_web_service_schema import ShipmentRequest as UPSShipmentRequest @@ -61,16 +64,19 @@ def create_shipment_request( ) -> Serializable[UPSShipmentRequest]: return shipment_request(payload, self.settings) + def create_cancel_shipment_request(self, payload: ShipmentCancelRequest) -> Serializable: + return void_shipment_request(payload, self.settings) + def create_pickup_request(self, payload: PickupRequest) -> Serializable[Pipeline]: return create_pickup_pipeline(payload, self.settings) - def create_modify_pickup_request( + def create_pickup_update_request( self, payload: PickupUpdateRequest ) -> Serializable[Pipeline]: return update_pickup_pipeline(payload, self.settings) def create_cancel_pickup_request( - self, payload: PickupCancellationRequest + self, payload: PickupCancelRequest ) -> Serializable[Envelope]: return cancel_pickup_request(payload, self.settings) @@ -96,12 +102,17 @@ def parse_shipment_response( ) -> Tuple[ShipmentDetails, List[Message]]: return parse_shipment_response(response.deserialize(), self.settings) + def parse_cancel_shipment_response( + self, response: Deserializable[str] + ) -> Tuple[ConfirmationDetails, List[Message]]: + return parse_void_shipment_response(response.deserialize(), self.settings) + def parse_pickup_response( self, response: Deserializable ) -> Tuple[PickupDetails, List[Message]]: return parse_pickup_response(response.deserialize(), self.settings) - def parse_modify_pickup_response( + def parse_pickup_update_response( self, response: Deserializable ) -> Tuple[PickupDetails, List[Message]]: return parse_pickup_response(response.deserialize(), self.settings) diff --git a/extensions/ups_package/purplship/mappers/ups_package/proxy.py b/extensions/ups_package/purplship/mappers/ups_package/proxy.py index 619817c4f4..c647a1da97 100644 --- a/extensions/ups_package/purplship/mappers/ups_package/proxy.py +++ b/extensions/ups_package/purplship/mappers/ups_package/proxy.py @@ -51,7 +51,12 @@ def create_shipment(self, request: Serializable[Envelope]) -> Deserializable[str return Deserializable(response, to_xml) - def request_pickup(self, request: Serializable[Pipeline]) -> Deserializable[str]: + def cancel_shipment(self, request: Serializable) -> Deserializable[str]: + response = self._send_request("/Ship", request) + + return Deserializable(response, to_xml) + + def schedule_pickup(self, request: Serializable[Pipeline]) -> Deserializable[str]: def process(job: Job): if job.data is None: return job.fallback diff --git a/extensions/ups_package/setup.py b/extensions/ups_package/setup.py index 860a02ee85..b3183277e6 100644 --- a/extensions/ups_package/setup.py +++ b/extensions/ups_package/setup.py @@ -3,7 +3,7 @@ setup( name="purplship.ups_package", - version="2020.9.0", + version="2020.10.0", description="Multi-carrier shipping API integration with python", url="https://github.com/PurplShip/purplship", author="Purplship Team", diff --git a/extensions/usps/setup.py b/extensions/usps/setup.py index 51f2c264d5..08e69538b3 100644 --- a/extensions/usps/setup.py +++ b/extensions/usps/setup.py @@ -3,7 +3,7 @@ setup( name="purplship.usps", - version="2020.9.0", + version="2020.10.0", description="Multi-carrier shipping API integration with python", url="https://github.com/PurplShip/purplship", author="Purplship Team", diff --git a/mypy.ini b/mypy.ini index aa369da550..31ef93744f 100644 --- a/mypy.ini +++ b/mypy.ini @@ -51,3 +51,6 @@ ignore_missing_imports = True [mypy-pypurolator.*] ignore_missing_imports = True + +[mypy-pycanpar.*] +ignore_missing_imports = True diff --git a/purplship/api/__init__.py b/purplship/api/__init__.py index 17654c3f1a..2304e60456 100644 --- a/purplship/api/__init__.py +++ b/purplship/api/__init__.py @@ -1,2 +1,2 @@ -"""PurplShip Universal API.""" +"""Purplship Universal API.""" __path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore diff --git a/purplship/api/gateway.py b/purplship/api/gateway.py index f9f7daee26..281b03c38f 100644 --- a/purplship/api/gateway.py +++ b/purplship/api/gateway.py @@ -1,4 +1,4 @@ -"""PurplShip API Gateway modules.""" +"""Purplship API Gateway modules.""" import attr import pkgutil @@ -31,7 +31,7 @@ def create(self, settings: dict) -> Gateway: class _ProviderMapper: @property def providers(self): - # Register PurplShip mappers + # Register Purplship mappers return { name: __import__(f"{mappers.__name__}.{name}", fromlist=[name]) for _, name, _ in pkgutil.iter_modules(mappers.__path__) @@ -62,7 +62,7 @@ def initializer(settings: Union[Settings, dict]) -> Gateway: gateway = _ProviderMapper() logger.info( f""" -PurplShip default gateway mapper initialized. +Purplship default gateway mapper initialized. Registered providers: {','.join(gateway.providers)} """ ) diff --git a/purplship/api/interface.py b/purplship/api/interface.py index 8f0738d86f..f5c857d262 100644 --- a/purplship/api/interface.py +++ b/purplship/api/interface.py @@ -13,9 +13,10 @@ ShipmentRequest, TrackingRequest, PickupRequest, - PickupCancellationRequest, + PickupCancelRequest, PickupUpdateRequest, Message, + ShipmentCancelRequest, ) logger = logging.getLogger(__name__) @@ -109,13 +110,13 @@ def deserialize(): class Pickup: @staticmethod - def book(args: Union[PickupRequest, dict]): + def schedule(args: Union[PickupRequest, dict]): logger.debug(f"book a pickup. payload: {jsonify(args)}") payload = args if isinstance(args, PickupRequest) else PickupRequest(**args) def action(gateway: Gateway): request: Serializable = gateway.mapper.create_pickup_request(payload) - response: Deserializable = gateway.proxy.request_pickup(request) + response: Deserializable = gateway.proxy.schedule_pickup(request) @fail_safe(gateway) def deserialize(): @@ -126,12 +127,12 @@ def deserialize(): return IRequestWith(action) @staticmethod - def cancel(args: Union[PickupCancellationRequest, dict]): + def cancel(args: Union[PickupCancelRequest, dict]): logger.debug(f"cancel a pickup. payload: {jsonify(args)}") payload = ( args - if isinstance(args, PickupCancellationRequest) - else PickupCancellationRequest(**args) + if isinstance(args, PickupCancelRequest) + else PickupCancelRequest(**args) ) def action(gateway: Gateway): @@ -156,12 +157,12 @@ def update(args: Union[PickupUpdateRequest, dict]): ) def action(gateway: Gateway): - request: Serializable = gateway.mapper.create_modify_pickup_request(payload) + request: Serializable = gateway.mapper.create_pickup_update_request(payload) response: Deserializable = gateway.proxy.modify_pickup(request) @fail_safe(gateway) def deserialize(): - return gateway.mapper.parse_modify_pickup_response(response) + return gateway.mapper.parse_pickup_update_response(response) return IDeserialize(deserialize) @@ -205,6 +206,23 @@ def deserialize(): return IRequestWith(action) + @staticmethod + def cancel(args: Union[ShipmentCancelRequest, dict]): + logger.debug(f"void a shipment. payload: {jsonify(args)}") + payload = args if isinstance(args, ShipmentCancelRequest) else ShipmentCancelRequest(**args) + + def action(gateway: Gateway): + request: Serializable = gateway.mapper.create_cancel_shipment_request(payload) + response: Deserializable = gateway.proxy.cancel_shipment(request) + + @fail_safe(gateway) + def deserialize(): + return gateway.mapper.parse_cancel_shipment_response(response) + + return IDeserialize(deserialize) + + return IRequestFrom(action) + class Tracking: @staticmethod diff --git a/purplship/api/mapper.py b/purplship/api/mapper.py index 6b3f4316e8..226977308d 100644 --- a/purplship/api/mapper.py +++ b/purplship/api/mapper.py @@ -1,4 +1,4 @@ -"""PurplShip Mapper base class definition module.""" +"""Purplship Mapper base class definition module.""" import attr from abc import ABC @@ -10,8 +10,9 @@ TrackingRequest, ShipmentDetails, ShipmentRequest, + ShipmentCancelRequest, PickupRequest, - PickupCancellationRequest, + PickupCancelRequest, PickupUpdateRequest, PickupDetails, RateDetails, @@ -33,15 +34,7 @@ class Mapper(ABC): def create_address_validation_request(self, payload: AddressValidationRequest) -> Serializable: """ Create a carrier specific address validation request data from the payload """ raise MethodNotSupportedError( - self.__class__.create_address_validation_request.__name__, self.__class__.__name__ - ) - - def parse_address_validation_response( - self, response: Deserializable - ) -> Tuple[AddressValidationDetails, List[Message]]: - """ Create a unified API address validation details from the carrier response """ - raise MethodNotSupportedError( - self.__class__.parse_address_validation_response.__name__, self.__class__.__name__ + self.__class__.create_address_validation_request.__name__, self.settings.carrier_name ) def create_rate_request(self, payload: RateRequest) -> Serializable: @@ -64,26 +57,16 @@ def create_tracking_request(self, payload: TrackingRequest) -> Serializable: self.__class__.create_tracking_request.__name__, self.settings.carrier_name ) - def parse_tracking_response( - self, response: Deserializable - ) -> Tuple[List[TrackingDetails], List[Message]]: - """ Create a unified API tracking result list from carrier response """ - raise MethodNotSupportedError( - self.__class__.parse_tracking_response.__name__, self.settings.carrier_name - ) - def create_shipment_request(self, payload: ShipmentRequest) -> Serializable: """ Create a carrier specific shipment creation request data from payload """ raise MethodNotSupportedError( self.__class__.create_shipment_request.__name__, self.settings.carrier_name ) - def parse_shipment_response( - self, response: Deserializable - ) -> Tuple[ShipmentDetails, List[Message]]: - """ Create a unified API shipment creation result from carrier response """ + def create_cancel_shipment_request(self, payload: ShipmentCancelRequest) -> Serializable: + """ Create a carrier specific void shipment request data from payload """ raise MethodNotSupportedError( - self.__class__.parse_shipment_response.__name__, self.settings.carrier_name + self.__class__.create_cancel_shipment_request.__name__, self.settings.carrier_name ) def create_pickup_request(self, payload: PickupRequest) -> Serializable: @@ -92,38 +75,64 @@ def create_pickup_request(self, payload: PickupRequest) -> Serializable: self.__class__.create_pickup_request.__name__, self.settings.carrier_name ) - def parse_pickup_response( - self, response: Deserializable - ) -> Tuple[PickupDetails, List[Message]]: - """ Create a unified API pickup result from carrier response """ + def create_pickup_update_request( + self, payload: PickupUpdateRequest + ) -> Serializable: + """ Create a carrier specific pickup modification request data from payload """ raise MethodNotSupportedError( - self.__class__.parse_pickup_response.__name__, self.settings.carrier_name + self.__class__.create_pickup_update_request.__name__, + self.settings.carrier_name, ) - def create_modify_pickup_request( - self, payload: PickupUpdateRequest + def create_cancel_pickup_request( + self, payload: PickupCancelRequest ) -> Serializable: - """ Create a carrier specific pickup modification request data from payload """ + """ Create a carrier specific pickup cancellation request data from payload """ raise MethodNotSupportedError( - self.__class__.create_modify_pickup_request.__name__, + self.__class__.create_cancel_pickup_request.__name__, self.settings.carrier_name, ) - def parse_modify_pickup_response( + """Response Parsers""" + + def parse_address_validation_response( + self, response: Deserializable + ) -> Tuple[AddressValidationDetails, List[Message]]: + """ Create a unified API address validation details from the carrier response """ + raise MethodNotSupportedError( + self.__class__.parse_address_validation_response.__name__, self.settings.carrier_name + ) + + def parse_shipment_response( + self, response: Deserializable + ) -> Tuple[ShipmentDetails, List[Message]]: + """ Create a unified API shipment creation result from carrier response """ + raise MethodNotSupportedError( + self.__class__.parse_shipment_response.__name__, self.settings.carrier_name + ) + + def parse_cancel_shipment_response( + self, response: Deserializable + ) -> Tuple[ConfirmationDetails, List[Message]]: + """ Create a unified API operation confirmation detail from the carrier response """ + raise MethodNotSupportedError( + self.__class__.parse_cancel_shipment_response.__name__, self.settings.carrier_name + ) + + def parse_pickup_response( self, response: Deserializable ) -> Tuple[PickupDetails, List[Message]]: """ Create a unified API pickup result from carrier response """ raise MethodNotSupportedError( - self.__class__.parse_modify_pickup_response.__name__, - self.settings.carrier_name, + self.__class__.parse_pickup_response.__name__, self.settings.carrier_name ) - def create_cancel_pickup_request( - self, payload: PickupCancellationRequest - ) -> Serializable: - """ Create a carrier specific pickup cancellation request data from payload """ + def parse_pickup_update_response( + self, response: Deserializable + ) -> Tuple[PickupDetails, List[Message]]: + """ Create a unified API pickup result from carrier response """ raise MethodNotSupportedError( - self.__class__.create_cancel_pickup_request.__name__, + self.__class__.parse_pickup_update_response.__name__, self.settings.carrier_name, ) @@ -135,3 +144,11 @@ def parse_cancel_pickup_response( self.__class__.parse_cancel_pickup_response.__name__, self.settings.carrier_name, ) + + def parse_tracking_response( + self, response: Deserializable + ) -> Tuple[List[TrackingDetails], List[Message]]: + """ Create a unified API tracking result list from carrier response """ + raise MethodNotSupportedError( + self.__class__.parse_tracking_response.__name__, self.settings.carrier_name + ) diff --git a/purplship/api/proxy.py b/purplship/api/proxy.py index 311379a3f6..f49d7ecf4a 100644 --- a/purplship/api/proxy.py +++ b/purplship/api/proxy.py @@ -1,4 +1,4 @@ -"""PurplShip Proxy base class definition module.""" +"""Purplship Proxy base class definition module.""" import attr from abc import ABC @@ -28,9 +28,14 @@ def create_shipment(self, request: Serializable) -> Deserializable: self.__class__.create_shipment.__name__, self.settings.carrier_name ) - def request_pickup(self, request: Serializable) -> Deserializable: + def cancel_shipment(self, request: Serializable) -> Deserializable: raise MethodNotSupportedError( - self.__class__.request_pickup.__name__, self.settings.carrier_name + self.__class__.cancel_shipment.__name__, self.settings.carrier_name + ) + + def schedule_pickup(self, request: Serializable) -> Deserializable: + raise MethodNotSupportedError( + self.__class__.schedule_pickup.__name__, self.settings.carrier_name ) def modify_pickup(self, request: Serializable) -> Deserializable: @@ -45,5 +50,5 @@ def cancel_pickup(self, request: Serializable) -> Deserializable: def validate_address(self, request: Serializable) -> Deserializable: raise MethodNotSupportedError( - self.__class__.validate_address.__name__, self.__class__.__name__ + self.__class__.validate_address.__name__, self.settings.carrier_name ) diff --git a/purplship/core/errors.py b/purplship/core/errors.py index 80237a5db6..f3ea34228a 100644 --- a/purplship/core/errors.py +++ b/purplship/core/errors.py @@ -1,4 +1,4 @@ -"""PurplShip Custom Errors(Exception) definition modules""" +"""Purplship Custom Errors(Exception) definition modules""" import warnings from enum import Enum from typing import Dict diff --git a/purplship/core/models.py b/purplship/core/models.py index 41f4c4d63a..10abb6d000 100644 --- a/purplship/core/models.py +++ b/purplship/core/models.py @@ -59,16 +59,6 @@ class Parcel: dimension_unit: str = None -@attr.s(auto_attribs=True) -class Invoice: - """invoice type.""" - - date: str - identifier: str = None - type: str = None - copies: int = None - - @attr.s(auto_attribs=True) class Card: """Credit Card type.""" @@ -92,20 +82,27 @@ class Payment: account_number: str = None credit_card: Card = JStruct[Card] contact: Address = JStruct[Address] + id: str = None @attr.s(auto_attribs=True) class Customs: """customs type.""" - no_eei: str = None aes: str = None - description: str = None - terms_of_trade: str = None + eel_pfc: str = None + certify: bool = None + signer: str = None + content_type: str = None + content_description: str = None + incoterm: str = None + invoice: str = None + certificate_number: str = None commodities: List[Commodity] = JList[Commodity] duty: Payment = JStruct[Payment] - invoice: Invoice = JStruct[Invoice] commercial_invoice: bool = False + options: Dict = {} + id: str = None @attr.s(auto_attribs=True) @@ -135,6 +132,16 @@ class ShipmentRequest: reference: str = "" +@attr.s(auto_attribs=True) +class ShipmentCancelRequest: + """shipment cancellation request type.""" + + shipment_identifier: str + + service: str = None + options: Dict = {} + + @attr.s(auto_attribs=True) class RateRequest: shipper: Address = JStruct[Address, REQUIRED] @@ -159,7 +166,7 @@ class TrackingRequest: class PickupRequest: """pickup request type.""" - date: str + pickup_date: str ready_time: str closing_time: str address: Address = JStruct[Address, REQUIRED] @@ -174,7 +181,7 @@ class PickupRequest: class PickupUpdateRequest: """pickup update request type.""" - date: str + pickup_date: str ready_time: str closing_time: str confirmation_number: str @@ -187,18 +194,15 @@ class PickupUpdateRequest: @attr.s(auto_attribs=True) -class PickupCancellationRequest: +class PickupCancelRequest: """pickup cancellation request type.""" confirmation_number: str - address: Address = JStruct[Address] # TODO:: Make this field REQUIRED + + address: Address = JStruct[Address] pickup_date: str = None reason: str = None - # Deprecated - person_name: str = None - country_code: str = None - @attr.s(auto_attribs=True) class AddressValidationRequest: @@ -221,7 +225,7 @@ class COD: class Notification: """notification option type.""" - email: str = None # Only defined if other email than shipper + email: str = None # Only defined if other email than recipient locale: str = "en" @@ -303,6 +307,7 @@ class TrackingDetails: carrier_id: str tracking_number: str events: List[TrackingEvent] = JList[TrackingEvent, REQUIRED] + delivered: bool = None @attr.s(auto_attribs=True) @@ -313,6 +318,7 @@ class ShipmentDetails: carrier_id: str label: str tracking_number: str + shipment_identifier: str selected_rate: RateDetails = JStruct[RateDetails] meta: dict = None id: str = None @@ -339,3 +345,4 @@ class ConfirmationDetails: carrier_name: str carrier_id: str success: bool + operation: str diff --git a/purplship/core/settings.py b/purplship/core/settings.py index 2386fd652c..011154207a 100644 --- a/purplship/core/settings.py +++ b/purplship/core/settings.py @@ -1,4 +1,4 @@ -"""PurplShip Settings base class definition""" +"""Purplship Settings base class definition""" import attr from typing import Optional diff --git a/purplship/core/utils/datetime.py b/purplship/core/utils/datetime.py index 309e38b97d..1fc89ced89 100644 --- a/purplship/core/utils/datetime.py +++ b/purplship/core/utils/datetime.py @@ -18,11 +18,13 @@ def format_date(date_str: str = None, current_format: str = "%Y-%m-%d"): return date.strftime("%Y-%m-%d") -def format_datetime(date_str: str = None, current_format: str = "%Y-%m-%d %H:%M:%S"): +def format_datetime( + date_str: str = None, current_format: str = "%Y-%m-%d %H:%M:%S", output_format: str = "%Y-%m-%d %H:%M:%S" +): date = to_date(date_str, current_format) if date is None: return None - return date.strftime("%Y-%m-%d %H:%M:%S") + return date.strftime(output_format) def format_time( diff --git a/purplship/core/utils/helpers.py b/purplship/core/utils/helpers.py index 26b4ac67b4..034269de5f 100644 --- a/purplship/core/utils/helpers.py +++ b/purplship/core/utils/helpers.py @@ -48,7 +48,7 @@ def gif_to_pdf(gif_str: str) -> str: return base64.b64encode(new_buffer.getvalue()).decode("utf-8") -def request(decoder: Callable = decode_bytes, **args) -> str: +def request(decoder: Callable = decode_bytes, on_error: Callable[[HTTPError], str] = None, **args) -> str: """Return an HTTP response body. make a http request (wrapper around Request method from built in urllib) @@ -67,8 +67,11 @@ def request(decoder: Callable = decode_bytes, **args) -> str: return res except HTTPError as e: logger.exception(e) - error = e.read().decode("utf-8") + if on_error is not None: + return on_error(e) + + error = e.read().decode("utf-8") logger.debug(f"error response content {error}") return error @@ -127,7 +130,7 @@ def bundle_xml(xml_strings: List[str]) -> str: {all the XML trees concatenated} """ - bundle = "".join([xml_tostring(to_xml(x)) for x in xml_strings if x != ""]) + bundle = "".join([xml_tostring(to_xml(x)) for x in xml_strings if x is not None and x != ""]) return f"{bundle}" diff --git a/purplship/core/utils/xml.py b/purplship/core/utils/xml.py index aa8be5ed45..e9f5b5c07b 100644 --- a/purplship/core/utils/xml.py +++ b/purplship/core/utils/xml.py @@ -1,4 +1,4 @@ -"""PurplShip lxml typing and utilities wrappers""" +"""Purplship lxml typing and utilities wrappers""" from lxml import etree diff --git a/script.sh b/script.sh index be75559d89..645ca2e0fb 100755 --- a/script.sh +++ b/script.sh @@ -71,7 +71,7 @@ check() { backup_wheels() { # shellcheck disable=SC2154 - [ -d "$wheels" ] && + [[ -d "$wheels" ]] && find . -not -path "*$ENV_DIR/*" -name \*.whl -exec mv {} "$wheels" \; && clean_builds } diff --git a/setup.py b/setup.py index 4c723ca6da..026b999db6 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ ] setup(name='purplship', - version='2020.9.0', + version='2020.10.0', description='Multi-carrier shipping API integration with python', long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/canadapost/pickup.py b/tests/canadapost/pickup.py index bf7d20adae..99207673cf 100644 --- a/tests/canadapost/pickup.py +++ b/tests/canadapost/pickup.py @@ -5,7 +5,7 @@ from purplship.core.models import ( PickupRequest, PickupUpdateRequest, - PickupCancellationRequest, + PickupCancelRequest, ) from tests.canadapost.fixture import gateway @@ -15,20 +15,28 @@ def setUp(self): self.maxDiff = None self.PickupRequest = PickupRequest(**pickup_data) self.PickupUpdateRequest = PickupUpdateRequest(**pickup_update_data) - self.PickupCancelRequest = PickupCancellationRequest(**pickup_cancel_data) + self.PickupCancelRequest = PickupCancelRequest(**pickup_cancel_data) def test_create_pickup_request(self): requests = gateway.mapper.create_pickup_request(self.PickupRequest) pipeline = requests.serialize() request = pipeline["create_pickup"](PickupAvailabilityResponseXML) + self.assertEqual(request.data.serialize(), PickupRequestXML) def test_update_pickup_request(self): - request = gateway.mapper.create_modify_pickup_request(self.PickupUpdateRequest) - content = request.serialize() - self.assertEqual(content["data"], PickupUpdateRequestXML) + requests = gateway.mapper.create_pickup_update_request(self.PickupUpdateRequest) + pipeline = requests.serialize() + update = pipeline["update_pickup"]() + details = pipeline["get_pickup"]("") + + self.assertEqual(update.data.serialize()["data"], PickupUpdateRequestXML) + self.assertEqual( + update.data.serialize()["pickuprequest"], + pickup_update_data["confirmation_number"], + ) self.assertEqual( - content["pickuprequest"], pickup_update_data["confirmation_number"] + details.data.serialize(), "/enab/2004381/pickuprequest/0074698052/details" ) def test_cancel_pickup_request(self): @@ -40,27 +48,33 @@ def test_cancel_pickup_request(self): def test_create_pickup(self): with patch("purplship.mappers.canadapost.proxy.http") as mocks: mocks.side_effect = [PickupAvailabilityResponseXML, PickupResponseXML] - purplship.Pickup.book(self.PickupRequest).with_(gateway) + purplship.Pickup.schedule(self.PickupRequest).with_(gateway) availability_call, create_call = mocks.call_args_list + self.assertEqual( availability_call[1]["url"], - f"{gateway.settings.server_url}/ad/pickup/pickupavailability/{self.PickupRequest.address.postal_code}", + f"{gateway.settings.server_url}/ad/pickup/pickupavailability/B3L2C2", ) self.assertEqual( create_call[1]["url"], - f"{gateway.settings.server_url}/enab/{gateway.settings.customer_number}/pickuprequest", + f"{gateway.settings.server_url}/enab/2004381/pickuprequest", ) def test_update_pickup(self): - with patch("purplship.mappers.canadapost.proxy.http") as mock: - mock.return_value = "" + with patch("purplship.mappers.canadapost.proxy.http") as mocks: + mocks.side_effect = ["", PickupDetailseResponseXML] purplship.Pickup.update(self.PickupUpdateRequest).from_(gateway) - url = mock.call_args[1]["url"] + update_call, get_call = mocks.call_args_list + self.assertEqual( - url, - f"{gateway.settings.server_url}/enab/{gateway.settings.customer_number}/pickuprequest/{self.PickupUpdateRequest.confirmation_number}", + update_call[1]["url"], + f"{gateway.settings.server_url}/enab/2004381/pickuprequest/0074698052", + ) + self.assertEqual( + get_call[1]["url"], + f"{gateway.settings.server_url}/enab/2004381/pickuprequest/0074698052/details", ) def test_cancel_pickup(self): @@ -71,24 +85,33 @@ def test_cancel_pickup(self): url = mock.call_args[1]["url"] self.assertEqual( url, - f"{gateway.settings.server_url}/enab/{gateway.settings.customer_number}/pickuprequest/{self.PickupCancelRequest.confirmation_number}", + f"{gateway.settings.server_url}/enab/2004381/pickuprequest/0074698052", ) def test_parse_pickup_response(self): with patch("purplship.mappers.canadapost.proxy.http") as mocks: mocks.side_effect = [PickupAvailabilityResponseXML, PickupResponseXML] parsed_response = ( - purplship.Pickup.book(self.PickupRequest).with_(gateway).parse() + purplship.Pickup.schedule(self.PickupRequest).with_(gateway).parse() ) self.assertListEqual(to_dict(parsed_response), ParsedPickupResponse) + def test_parse_pickup_update_response(self): + with patch("purplship.mappers.canadapost.proxy.http") as mocks: + mocks.side_effect = [None, PickupDetailseResponseXML] + parsed_response = ( + purplship.Pickup.update(self.PickupUpdateRequest).from_(gateway).parse() + ) + + self.assertListEqual(to_dict(parsed_response), ParsedPickupUpdateResponse) + if __name__ == "__main__": unittest.main() pickup_data = { - "date": "2015-01-28", + "pickup_date": "2015-01-28", "address": { "company_name": "Jim Duggan", "address_line1": "2271 Herring Cove", @@ -109,7 +132,7 @@ def test_parse_pickup_response(self): pickup_update_data = { "confirmation_number": "0074698052", - "date": "2015-01-28", + "pickup_date": "2015-01-28", "address": { "person_name": "Jane Doe", "email": "john.doe@canadapost.ca", @@ -134,6 +157,15 @@ def test_parse_pickup_response(self): [], ] +ParsedPickupUpdateResponse = [ + { + "carrier_id": "canadapost", + "carrier_name": "canadapost", + "confirmation_number": "0074698052", + }, + [], +] + PickupRequestXML = """ 2004381 OnDemand @@ -169,11 +201,6 @@ def test_parse_pickup_response(self): """ PickupUpdateRequestXML = """ - 2004381 - OnDemand - - true - Jane Doe john.doe@canadapost.ca @@ -228,3 +255,50 @@ def test_parse_pickup_response(self): """ + +PickupDetailseResponseXML = f""" + + 0074698052 + Active + OnDemand + 2015-01-01 + + + + false + + Jim Duggan + 2271 Herring Cove + Halifax + NS + B3L2C2 + + + + John Doe + john.doe@canadapost.ca + 800-555-1212 + true + + + false + true + Door at Back + + + true + false + true + true + + 50 + + + 2015-01-28 + 15:00 + 17:00 + + + + +""" diff --git a/tests/canadapost/shipment.py b/tests/canadapost/shipment.py index c42a51489d..0e4345a720 100644 --- a/tests/canadapost/shipment.py +++ b/tests/canadapost/shipment.py @@ -2,7 +2,7 @@ from unittest.mock import patch import purplship from purplship.core.utils.helpers import to_dict -from purplship.core.models import ShipmentRequest +from purplship.core.models import ShipmentRequest, ShipmentCancelRequest from tests.canadapost.fixture import gateway, LabelResponse @@ -10,6 +10,7 @@ class TestCanadaPostShipment(unittest.TestCase): def setUp(self): self.maxDiff = None self.ShipmentRequest = ShipmentRequest(**shipment_data) + self.ShipmentCancelRequest = ShipmentCancelRequest(**shipment_cancel_data) def test_create_shipment_request(self): requests = gateway.mapper.create_shipment_request(self.ShipmentRequest) @@ -47,6 +48,18 @@ def test_create_shipment(self): f"{gateway.settings.server_url}/rs/{gateway.settings.customer_number}/{gateway.settings.customer_number}/shipment", ) + def test_cancel_shipment(self): + with patch("purplship.mappers.canadapost.proxy.http") as mock: + mock.return_value = "" + + purplship.Shipment.cancel(self.ShipmentCancelRequest).from_(gateway) + + url = mock.call_args[1]["url"] + self.assertEqual( + url, + f"{gateway.settings.server_url}/rs/2004381/2004381/shipment/123456789012", + ) + def test_parse_shipment_response(self): with patch("purplship.mappers.canadapost.proxy.http") as mocks: mocks.side_effect = [ShipmentResponseXML, LabelResponse] @@ -56,10 +69,25 @@ def test_parse_shipment_response(self): self.assertEqual(to_dict(parsed_response), to_dict(ParsedShipmentResponse)) + def test_parse_shipment_cancel_response(self): + with patch("purplship.mappers.canadapost.proxy.http") as mock: + mock.return_value = "" + parsed_response = ( + purplship.Shipment.cancel(self.ShipmentCancelRequest) + .from_(gateway) + .parse() + ) + + self.assertEqual( + to_dict(parsed_response), to_dict(ParsedShipmentCancelResponse) + ) + if __name__ == "__main__": unittest.main() +shipment_cancel_data = {"shipment_identifier": "123456789012"} + shipment_data = { "shipper": { "company_name": "CGI", @@ -98,7 +126,6 @@ def test_parse_shipment_response(self): }, } - shipment_with_package_preset_data = { "shipper": { "company_name": "CGI", @@ -130,13 +157,23 @@ def test_parse_shipment_response(self): "options": {"cash_on_delivery": {"amount": 25.5}}, } - ParsedShipmentResponse = [ { "carrier_name": "canadapost", "carrier_id": "canadapost", "label": LabelResponse, "tracking_number": "123456789012", + "shipment_identifier": "123456789012", + }, + [], +] + +ParsedShipmentCancelResponse = [ + { + "carrier_id": "canadapost", + "carrier_name": "canadapost", + "operation": "Cancel Shipment", + "success": True, }, [], ] @@ -152,7 +189,6 @@ def test_parse_shipment_response(self): "method": "GET", } - ShipmentPriceLinkXML = """ """ diff --git a/tests/canpar/__init__.py b/tests/canpar/__init__.py new file mode 100644 index 0000000000..853caecb21 --- /dev/null +++ b/tests/canpar/__init__.py @@ -0,0 +1,5 @@ +from tests.canpar.tracking import * +from tests.canpar.rate import * +from tests.canpar.shipment import * +from tests.canpar.pickup import * +from tests.canpar.address import * diff --git a/tests/canpar/address.py b/tests/canpar/address.py new file mode 100644 index 0000000000..39079f494c --- /dev/null +++ b/tests/canpar/address.py @@ -0,0 +1,136 @@ +import unittest +import logging +from unittest.mock import patch +import purplship +from purplship.core.utils.helpers import to_dict +from purplship.core.models import AddressValidationRequest +from tests.canpar.fixture import gateway + + +class TestCanparAddressValidation(unittest.TestCase): + def setUp(self): + self.maxDiff = None + self.AddressValidationRequest = AddressValidationRequest( + **AddressValidationPayload + ) + + def test_create_address_validation_request(self): + request = gateway.mapper.create_address_validation_request( + self.AddressValidationRequest + ) + + self.assertEqual( + request.serialize(), + AddressValidationRequestXML, + ) + + def test_validate_address(self): + with patch("purplship.mappers.canpar.proxy.http") as mock: + mock.return_value = "" + purplship.Address.validate(self.AddressValidationRequest).from_(gateway) + + self.assertEqual( + mock.call_args[1]["url"], + f"{gateway.settings.server_url}/CanparRatingService.CanparRatingServiceHttpSoap12Endpoint/", + ) + self.assertEqual( + mock.call_args[1]["headers"]["soapaction"], "urn:searchCanadaPost" + ) + + def test_parse_address_validation_response(self): + with patch("purplship.mappers.canpar.proxy.http") as mock: + mock.return_value = AddressValidationResponseXML + parsed_response = ( + purplship.Address.validate(self.AddressValidationRequest) + .from_(gateway) + .parse() + ) + + self.assertEqual( + to_dict(parsed_response), to_dict(ParsedAddressValidationResponse) + ) + + +if __name__ == "__main__": + unittest.main() + + +AddressValidationPayload = { + "address": { + "address_line1": "Suit 333", + "address_line2": "333 Twin", + "postal_code": "V5E4H9", + "city": "Burnaby", + "country_code": "CA", + "state_code": "BC", + } +} + +ParsedAddressValidationResponse = [ + { + "carrier_id": "canpar", + "carrier_name": "canpar", + "complete_address": { + "address_line1": "565 SHERBOURNE ST", + "city": "TORONTO", + "country_code": "CA", + "postal_code": "M4X1W7", + "residential": False, + "state_code": "ON", + }, + "success": True, + }, + [], +] + + +AddressValidationRequestXML = """ + + + + Burnaby + password + V5E4H9 + BC + + Suit 333 333 Twin + + + user_id + true + + + + +""" + +AddressValidationResponseXML = """ + + + + + + + 565 SHERBOURNE ST + + + + TORONTO + CA + + + -1 + 2012-06-15T15:42:40.044Z + + + M4X1W7 + ON + false + 2012-06-15T15:42:40.044Z + + + + + + +""" diff --git a/tests/canpar/fixture.py b/tests/canpar/fixture.py new file mode 100644 index 0000000000..06ddc52e1e --- /dev/null +++ b/tests/canpar/fixture.py @@ -0,0 +1,8 @@ +import purplship + +gateway = purplship.gateway["canpar"].create( + dict( + user_id="user_id", + password="password", + ) +) diff --git a/tests/canpar/pickup.py b/tests/canpar/pickup.py new file mode 100644 index 0000000000..2de4b4b67a --- /dev/null +++ b/tests/canpar/pickup.py @@ -0,0 +1,305 @@ +import logging +import unittest +from unittest.mock import patch +import purplship +from purplship.core.utils import to_dict +from purplship.core.models import ( + PickupRequest, + PickupUpdateRequest, + PickupCancelRequest, +) +from tests.canpar.fixture import gateway + +logger = logging.getLogger(__name__) + + +class TestCanparPickup(unittest.TestCase): + def setUp(self): + self.maxDiff = None + self.PickupRequest = PickupRequest(**pickup_data) + self.PickupUpdateRequest = PickupUpdateRequest(**pickup_update_data) + self.PickupCancelRequest = PickupCancelRequest(**pickup_cancel_data) + + def test_create_pickup_request(self): + request = gateway.mapper.create_pickup_request(self.PickupRequest) + + self.assertEqual(request.serialize(), PickupRequestXML) + + def test_create_modify_pickup_request(self): + request = gateway.mapper.create_pickup_update_request(self.PickupUpdateRequest) + + pipeline = request.serialize() + cancel_request = pipeline["cancel"]() + schedule_request = pipeline["schedule"](PickupCancelResponseXML) + + self.assertEqual(cancel_request.data.serialize(), PickupCancelRequestXML) + self.assertEqual(schedule_request.data.serialize(), PickupUpdateRequestXML) + + def test_create_cancel_pickup_request(self): + request = gateway.mapper.create_cancel_pickup_request(self.PickupCancelRequest) + + self.assertEqual(request.serialize(), PickupCancelRequestXML) + + def test_request_pickup(self): + with patch("purplship.mappers.canpar.proxy.http") as mock: + mock.return_value = "" + purplship.Pickup.schedule(self.PickupRequest).with_(gateway) + + self.assertEqual( + mock.call_args[1]["url"], + f"{gateway.settings.server_url}/CanparAddonsService.CanparAddonsServiceHttpSoap12Endpoint/", + ) + self.assertEqual( + mock.call_args[1]["headers"]["soapaction"], "urn:schedulePickupV2" + ) + + def test_modify_pickup(self): + with patch("purplship.mappers.canpar.proxy.http") as mocks: + mocks.side_effect = [ + PickupCancelResponseXML, + PickupResponseXML, + ] + purplship.Pickup.update(self.PickupUpdateRequest).from_(gateway) + + cancel_call, schedule_call = mocks.call_args_list + + self.assertEqual( + cancel_call[1]["url"], + f"{gateway.settings.server_url}/CanparAddonsService.CanparAddonsServiceHttpSoap12Endpoint/", + ) + self.assertEqual( + cancel_call[1]["headers"]["soapaction"], "urn:cancelPickup" + ) + self.assertEqual( + schedule_call[1]["url"], + f"{gateway.settings.server_url}/CanparAddonsService.CanparAddonsServiceHttpSoap12Endpoint/", + ) + self.assertEqual( + schedule_call[1]["headers"]["soapaction"], "urn:schedulePickupV2" + ) + + def test_cancel_pickup(self): + with patch("purplship.mappers.canpar.proxy.http") as mock: + mock.return_value = "" + purplship.Pickup.cancel(self.PickupCancelRequest).from_(gateway) + + self.assertEqual( + mock.call_args[1]["url"], + f"{gateway.settings.server_url}/CanparAddonsService.CanparAddonsServiceHttpSoap12Endpoint/", + ) + self.assertEqual( + mock.call_args[1]["headers"]["soapaction"], "urn:cancelPickup" + ) + + def test_parse_request_pickup_response(self): + with patch("purplship.mappers.canpar.proxy.http") as mock: + mock.return_value = PickupResponseXML + parsed_response = ( + purplship.Pickup.schedule(self.PickupRequest).with_(gateway).parse() + ) + + self.assertEqual(to_dict(parsed_response), to_dict(ParsedPickupResponse)) + + def test_parse_modify_pickup_response(self): + with patch("purplship.mappers.canpar.proxy.http") as mocks: + mocks.side_effect = [ + PickupCancelResponseXML, + PickupResponseXML, + ] + parsed_response = ( + purplship.Pickup.update(self.PickupUpdateRequest).from_(gateway).parse() + ) + + self.assertEqual(to_dict(parsed_response), to_dict(ParsedPickupResponse)) + + def test_parse_void_shipment_response(self): + with patch("purplship.mappers.canpar.proxy.http") as mock: + mock.return_value = PickupCancelResponseXML + parsed_response = ( + purplship.Pickup.cancel(self.PickupCancelRequest).from_(gateway).parse() + ) + + self.assertEqual( + to_dict(parsed_response), to_dict(ParsedPickupCancelResponse) + ) + + +if __name__ == "__main__": + unittest.main() + +pickup_data = { + "pickup_date": "2015-01-28", + "address": { + "company_name": "Jim Duggan", + "address_line1": "2271 Herring Cove", + "city": "Halifax", + "postal_code": "B3L2C2", + "country_code": "CA", + "person_name": "John Doe", + "phone_number": "1 514 5555555", + "state_code": "NS", + "residential": True, + "email": "john.doe@canpar.ca", + }, + "instruction": "Door at Back", + "ready_time": "15:00", + "closing_time": "17:00", +} + +pickup_update_data = { + "confirmation_number": "10000696", + "pickup_date": "2015-01-28", + "address": { + "person_name": "Jane Doe", + "email": "john.doe@canpar.ca", + "phone_number": "1 514 5555555", + }, + "parcels": [{"weight": 24, "weight_unit": "KG"}], + "instruction": "Door at Back", + "ready_time": "15:00", + "closing_time": "17:00", + "options": {"LoadingDockAvailable": False, "TrailerAccessible": False}, +} + +pickup_cancel_data = {"confirmation_number": "10000696"} + +ParsedPickupResponse = [ + { + "carrier_id": "canpar", + "carrier_name": "canpar", + "confirmation_number": "10000696", + "pickup_date": "2015-01-28 15:00:00", + }, + [], +] + +ParsedPickupCancelResponse = [ + { + "carrier_id": "canpar", + "carrier_name": "canpar", + "operation": "Cancel Pickup", + "success": True, + }, + [], +] + + +PickupRequestXML = """ + + + + password + + Door at Back + John Doe + + 2271 Herring Cove + + John Doe + Halifax + CA + john.doe@canpar.ca + Jim Duggan + 1 514 5555555 + B3L2C2 + NS + true + + 2015-01-28T15:00:00 + 1 514 5555555 + L + + user_id + + + + +""" + +PickupUpdateRequestXML = """ + + + + password + + Door at Back + Jane Doe + + + + Jane Doe + john.doe@canpar.ca + 1 514 5555555 + false + + 2015-01-28T15:00:00 + 1 514 5555555 + L + 52.909999999999997 + + user_id + + + + +""" + +PickupCancelRequestXML = """ + + + + 10000696 + password + user_id + + + + +""" + +PickupCancelResponseXML = """ + + + + + + + + + +""" + +PickupResponseXML = """ + + + + + + + 10000696 + Door at Back + John Doe + + 2271 Herring Cove + + John Doe + Halifax + CA + john.doe@canpar.ca + Jim Duggan + 1 514 5555555 + B3L2C2 + NS + true + + 2015-01-28T15:00:00 + 1 514 5555555 + L + + + + + +""" diff --git a/tests/canpar/rate.py b/tests/canpar/rate.py new file mode 100644 index 0000000000..599e6eda43 --- /dev/null +++ b/tests/canpar/rate.py @@ -0,0 +1,285 @@ +import re +import unittest +import logging +from unittest.mock import patch +from purplship.core.utils.helpers import to_dict +from purplship import Rating +from purplship.core.models import RateRequest +from tests.canpar.fixture import gateway + + +class TestCanparRating(unittest.TestCase): + def setUp(self): + self.maxDiff = None + self.RateRequest = RateRequest(**RatePayload) + + def test_create_rate_request(self): + request = gateway.mapper.create_rate_request(self.RateRequest) + serialized_request = re.sub( + "[^>]+", + "", + request.serialize(), + ) + + self.assertEqual(serialized_request, RateRequestXML) + + def test_get_rates(self): + with patch("purplship.mappers.canpar.proxy.http") as mock: + mock.return_value = "" + Rating.fetch(self.RateRequest).from_(gateway) + + self.assertEqual( + mock.call_args[1]["url"], + f"{gateway.settings.server_url}/CanparRatingService.CanparRatingServiceHttpSoap12Endpoint/", + ) + self.assertEqual( + mock.call_args[1]["headers"]["soapaction"], "urn:rateShipment" + ) + + def test_parse_rate_response(self): + with patch("purplship.mappers.canpar.proxy.http") as mock: + mock.return_value = RateResponseXml + parsed_response = Rating.fetch(self.RateRequest).from_(gateway).parse() + + self.assertEqual(to_dict(parsed_response), to_dict(ParsedQuoteResponse)) + + +if __name__ == "__main__": + unittest.main() + +RatePayload = { + "shipper": { + "company_name": "CGI", + "address_line1": "502 MAIN ST N", + "city": "MONTREAL", + "postal_code": "H2B1A0", + "country_code": "CA", + "person_name": "Bob", + "phone_number": "1 (450) 823-8432", + "state_code": "QC", + "residential": False, + }, + "recipient": { + "address_line1": "1 TEST ST", + "city": "TORONTO", + "company_name": "TEST ADDRESS", + "phone_number": "4161234567", + "postal_code": "M4X1W7", + "state_code": "ON", + "residential": False, + }, + "parcels": [ + { + "height": 3, + "length": 10, + "width": 3, + "weight": 1.0, + } + ], + "services": ["canpar_ground"], + "options": { + "canpar_extra_care": True, + }, +} + +ParsedQuoteResponse = [ + [ + { + "base_charge": 7.57, + "carrier_id": "canpar", + "carrier_name": "canpar", + "currency": "CAD", + "duties_and_taxes": 1.34, + "extra_charges": [ + {"amount": 7.57, "currency": "CAD", "name": "Freight Charge"}, + { + "amount": 2.75, + "currency": "CAD", + "name": "Residential Address Surcharge", + }, + {"amount": 1.34, "currency": "CAD", "name": "ONHST Tax Charge"}, + ], + "service": "canpar_ground", + "total_charge": 11.66, + "transit_days": 1, + } + ], + [], +] + + +RateRequestXML = """ + + + + false + false + false + password + + + 1 TEST ST + + TORONTO + TEST ADDRESS + 4161234567 + M4X1W7 + ON + false + + I + + 1.18 + 3.94 + 1. + 1.18 + true + + + 502 MAIN ST N + + Bob + MONTREAL + CA + CGI + 1 (450) 823-8432 + H2B1A0 + QC + false + + L + 1 + + + user_id + + + + +""" + +RateResponseXml = """ + + + + + + + + + 1.0 + L + 0.0 + N + + + false + 0.0 + + A1 + 1 TEST ST + + + + TORONTO + CA + 1@1.COM,2@2.COM + 23 + -1 + 2012-06-15T15:42:39.278Z + TEST ADDRESS + 4161234567 + M4X1W7 + ON + false + 2012-06-15T15:42:39.278Z + + + false + 0.0 + I + 0.0 + 0.0 + 0 + 2012-06-18T04:00:00.000Z + 7.57 + 0.0 + 0.0 + $ + -1 + 2012-06-15T15:42:39.278Z + + + false + + + + 1.0 + + + 0.0 + 0.0 + false + 0.0 + -1 + 2012-06-15T15:42:39.278Z + 0.0 + false + 0 + 0 + + 1.0 + + 2012-06-15T15:42:39.278Z + 0.0 + false + + + A1 + 1 TEST ST + + + + TORONTO + CA + 1@1.COM,2@2.COM + 23 + -1 + 2012-06-15T15:42:39.278Z + TEST ADDRESS + 4161234567 + M4X1W7 + ON + false + 2012-06-15T15:42:39.278Z + + N + 0.0 + + 2.75 + L + 0.0 + false + false + 1 + R + 99999999 + 2012-06-15T04:00:00.000Z + 1.34 + 0.0 + ONHST + + 1 + false + 2012-06-15T15:42:39.278Z + WSADMIN@DEMO.COM + false + 0.0 + 1 + + + + + + +""" diff --git a/tests/canpar/shipment.py b/tests/canpar/shipment.py new file mode 100644 index 0000000000..59e50d326c --- /dev/null +++ b/tests/canpar/shipment.py @@ -0,0 +1,414 @@ +import re +import unittest +import logging +from unittest.mock import patch +import purplship +from purplship.core.utils import to_dict +from purplship.core.models import ShipmentRequest, ShipmentCancelRequest +from tests.canpar.fixture import gateway + + +class TestCanparShipment(unittest.TestCase): + def setUp(self): + self.maxDiff = None + self.ShipmentRequest = ShipmentRequest(**shipment_data) + self.VoidShipmentRequest = ShipmentCancelRequest(**void_shipment_data) + + def test_create_shipment_request(self): + request = gateway.mapper.create_shipment_request(self.ShipmentRequest) + + pipeline = request.serialize() + process_shipment_request = pipeline["process"]() + get_label_request = pipeline["get_label"](ShipmentResponseXML) + + serialized_process_shipment_request = re.sub( + "[^>]+", + "", + process_shipment_request.data.serialize(), + ) + + self.assertEqual(serialized_process_shipment_request, ShipmentRequestXML) + self.assertEqual(get_label_request.data.serialize(), ShipmentLabelRequestXML) + + def test_create_void_shipment_request(self): + request = gateway.mapper.create_cancel_shipment_request( + self.VoidShipmentRequest + ) + + self.assertEqual(request.serialize(), VoidShipmentRequestXML) + + def test_create_shipment(self): + with patch("purplship.mappers.canpar.proxy.http") as mocks: + mocks.side_effect = [ + ShipmentResponseXML, + ShipmentLabelResponseXML, + ] + purplship.Shipment.create(self.ShipmentRequest).with_(gateway) + + process_shipment_call, get_label_call = mocks.call_args_list + + self.assertEqual( + process_shipment_call[1]["url"], + f"{gateway.settings.server_url}/CanshipBusinessService.CanshipBusinessServiceHttpSoap12Endpoint/", + ) + self.assertEqual( + process_shipment_call[1]["headers"]["soapaction"], "urn:processShipment" + ) + self.assertEqual( + get_label_call[1]["url"], + f"{gateway.settings.server_url}/CanshipBusinessService.CanshipBusinessServiceHttpSoap12Endpoint/", + ) + self.assertEqual( + get_label_call[1]["headers"]["soapaction"], "urn:getLabels" + ) + + def test_void_shipment(self): + with patch("purplship.mappers.canpar.proxy.http") as mock: + mock.return_value = "" + purplship.Shipment.cancel(self.VoidShipmentRequest).from_(gateway) + + self.assertEqual( + mock.call_args[1]["url"], + f"{gateway.settings.server_url}/CanshipBusinessService.CanshipBusinessServiceHttpSoap12Endpoint/", + ) + self.assertEqual( + mock.call_args[1]["headers"]["soapaction"], "urn:voidShipment" + ) + + def test_parse_shipment_response(self): + with patch("purplship.mappers.canpar.proxy.http") as mocks: + mocks.side_effect = [ + ShipmentResponseXML, + ShipmentLabelResponseXML, + ] + parsed_response = ( + purplship.Shipment.create(self.ShipmentRequest).with_(gateway).parse() + ) + + self.assertEqual(to_dict(parsed_response), to_dict(ParsedShipmentResponse)) + + def test_parse_void_shipment_response(self): + with patch("purplship.mappers.canpar.proxy.http") as mock: + mock.return_value = VoidShipmentResponseXML + parsed_response = ( + purplship.Shipment.cancel(self.VoidShipmentRequest) + .from_(gateway) + .parse() + ) + + self.assertEqual( + to_dict(parsed_response), to_dict(ParsedVoidShipmentResponse) + ) + + +if __name__ == "__main__": + unittest.main() + + +void_shipment_data = {"shipment_identifier": "10000696"} + +shipment_data = { + "shipper": { + "company_name": "CGI", + "address_line1": "502 MAIN ST N", + "city": "MONTREAL", + "postal_code": "H2B1A0", + "country_code": "CA", + "person_name": "Bob", + "phone_number": "1 (450) 823-8432", + "state_code": "QC", + "residential": False, + }, + "recipient": { + "address_line1": "1 TEST ST", + "city": "TORONTO", + "company_name": "TEST ADDRESS", + "phone_number": "4161234567", + "postal_code": "M4X1W7", + "state_code": "ON", + "residential": False, + }, + "parcels": [ + { + "height": 3, + "length": 10, + "width": 3, + "weight": 1.0, + } + ], + "service": "canpar_ground", + "options": { + "canpar_extra_care": True, + }, +} + +ParsedShipmentResponse = [ + { + "carrier_id": "canpar", + "carrier_name": "canpar", + "label": "...ENCODED INFORMATION...", + "selected_rate": { + "base_charge": 7.57, + "carrier_id": "canpar", + "carrier_name": "canpar", + "currency": "CAD", + "duties_and_taxes": 1.34, + "extra_charges": [ + {"amount": 7.57, "currency": "CAD", "name": "Freight Charge"}, + { + "amount": 2.75, + "currency": "CAD", + "name": "Residential Address Surcharge", + }, + {"amount": 1.34, "currency": "CAD", "name": "ONHST Tax Charge"}, + ], + "service": "canpar_ground", + "total_charge": 11.66, + "transit_days": 1, + }, + "tracking_number": "D999999990000000461001", + "shipment_identifier": "10000696", + }, + [], +] + +ParsedVoidShipmentResponse = [ + { + "carrier_id": "canpar", + "carrier_name": "canpar", + "operation": "Cancel Shipment", + "success": True, + }, + [], +] + + +ShipmentRequestXML = """ + + + + password + + + 1 TEST ST + + TORONTO + TEST ADDRESS + 4161234567 + M4X1W7 + ON + false + + I + + 1.18 + 3.94 + 1. + 1.18 + true + + + 502 MAIN ST N + + Bob + MONTREAL + CA + CGI + 1 (450) 823-8432 + H2B1A0 + QC + false + + L + Service.canpar_ground + + + user_id + + + + +""" + +ShipmentLabelRequestXML = """ + + + + false + 10000696 + password + false + user_id + + + + +""" + +ShipmentResponseXML = """ + + + + + + + + 1.0 + L + 0.0 + N + + + false + 0.0 + + A1 + 1 TEST ST + + + + TORONTO + CA + 1@1.COM,2@2.COM + 23 + 10001951 + 2012-06-15T15:41:11.763Z + TEST ADDRESS + 4161234567 + M4X1W7 + ON + false + 2012-06-15T15:41:11.998Z + + + false + 0.0 + I + 0.0 + 0.0 + 0 + 2012-06-18T04:00:00.000Z + 7.57 + 0.0 + 0.0 + $ + 10000696 + 2012-06-15T15:41:11.763Z + + + false + + + D999999990000000461001 + 1.0 + + + 0.0 + 0.0 + false + 0.0 + 10001802 + 2012-06-15T15:41:11.763Z + 0.0 + false + 1 + 461 + + 1.0 + + 2012-06-15T15:41:11.998Z + 0.0 + false + + + A1 + 1 TEST ST + + + + TORONTO + CA + 1@1.COM,2@2.COM + 23 + 10001950 + 2012-06-15T15:41:11.763Z + TEST ADDRESS + 4161234567 + L6T0G8 + ON + false + 2012-06-15T15:41:11.998Z + + N + 0.0 + + 2.75 + L + 0.0 + false + false + 1 + R + 99999999 + 2012-06-15T04:00:00.000Z + 1.34 + 0.0 + ONHST + + 1 + false + 2012-06-15T15:41:11.998Z + user_id + false + 0.0 + 1 + + + + + + +""" + +ShipmentLabelResponseXML = """ + + + + + ...ENCODED INFORMATION... + + + + +""" + +VoidShipmentRequestXML = """ + + + + 10000696 + password + user_id + + + + +""" + +VoidShipmentResponseXML = """ + + + + + + + + + +""" diff --git a/tests/canpar/tracking.py b/tests/canpar/tracking.py new file mode 100644 index 0000000000..446c279e53 --- /dev/null +++ b/tests/canpar/tracking.py @@ -0,0 +1,287 @@ +import unittest +import logging +from unittest.mock import patch +from purplship.core.utils.helpers import to_dict +from purplship import Tracking +from purplship.core.models import TrackingRequest +from tests.canpar.fixture import gateway + + +class TestCanparTracking(unittest.TestCase): + def setUp(self): + self.maxDiff = None + self.TrackingRequest = TrackingRequest(tracking_numbers=TRACKING_PAYLOAD) + + def test_create_tracking_request(self): + request = gateway.mapper.create_tracking_request(self.TrackingRequest) + + self.assertEqual(request.serialize()[0], TrackingRequestXML) + + def test_get_tracking(self): + with patch("purplship.mappers.canpar.proxy.http") as mock: + mock.return_value = "" + Tracking.fetch(self.TrackingRequest).from_(gateway) + + self.assertEqual( + mock.call_args[1]["url"], + f"{gateway.settings.server_url}/CanparAddonsService.CanparAddonsServiceHttpSoap12Endpoint/", + ) + self.assertEqual( + mock.call_args[1]["headers"]["soapaction"], "urn:trackByBarcodeV2" + ) + + def test_parse_tracking_response(self): + with patch("purplship.mappers.canpar.proxy.http") as mock: + mock.return_value = TrackingResponseXML + parsed_response = ( + Tracking.fetch(self.TrackingRequest).from_(gateway).parse() + ) + + self.assertEqual(to_dict(parsed_response), to_dict(ParsedTrackingResponse)) + + +if __name__ == "__main__": + unittest.main() + +TRACKING_PAYLOAD = ["1Z12345E6205277936"] + +ParsedTrackingResponse = [ + [ + { + "carrier_id": "canpar", + "carrier_name": "canpar", + "delivered": True, + "events": [ + { + "code": "DEL", + "date": "2011-03-08", + "description": "DELIVERED", + "location": "1785 FROBISHER ST, UNIT C, SUDBURY, ON, CA", + "time": "09:14", + }, + { + "code": "INA", + "date": "2011-03-08", + "description": "ARRIVAL AT INTERLINE", + "location": "1785 FROBISHER ST, UNIT C, SUDBURY, ON, CA", + "time": "06:15", + }, + { + "code": "INO", + "date": "2011-03-07", + "description": "INTERLINE OUTBOUND", + "location": "JOHN CYOPECK CENTRE, 205 NEW TORONTO STREET, ETOBICOKE, ON, CA", + "time": "23:05", + }, + { + "code": "ARR", + "date": "2011-03-07", + "description": "ARRIVAL AT HUB/TERMINAL FROM BULK/CITY TRAILER", + "location": "JOHN CYOPECK CENTRE, 205 NEW TORONTO STREET, ETOBICOKE, ON, CA", + "time": "23:03", + }, + { + "code": "PIC", + "date": "2011-03-07", + "description": "PICKUP FROM CUSTOMER/SHIPPER", + "location": "JOHN CYOPECK CENTRE, 205 NEW TORONTO STREET, ETOBICOKE, ON, CA", + "time": "14:53", + }, + ], + "tracking_number": "D999999988030400000008", + } + ], + [], +] + +TrackingRequestXML = """ + + + + 1Z12345E6205277936 + true + + + + +""" + +TrackingResponseXML = """ + + + + + + + D999999988030400000008 + + + 199 LARCH ST + 3RD FLOOR (PRINT SHOP) + + + SUDBURY + CA + + + -1 + 2015-01-22T23:17:26.681Z + + + P3E5P9 + ON + false + 2015-01-22T23:17:26.681Z + + false + + + + + 1785 FROBISHER ST, UNIT C + + + + SUDBURY + CA + + + -1 + 2015-01-22T23:17:26.681Z + + + P3A6C8 + ON + false + 2015-01-22T23:17:26.681Z + + DEL + DELIVERED + LIVR… + + 20110308 091442 + 3 + + + + + 1785 FROBISHER ST, UNIT C + + + + SUDBURY + CA + + + -1 + 2015-01-22T23:17:26.681Z + + + P3A6C8 + ON + false + 2015-01-22T23:17:26.681Z + + INA + ARRIVAL AT INTERLINE + ARRIV… ¿ LA FIRME DE TRANSPORT DE LIAISON + + 20110308 061542 + 3 + + + + + JOHN CYOPECK CENTRE + 205 NEW TORONTO STREET + + + ETOBICOKE + CA + + + -1 + 2015-01-22T23:17:26.681Z + + + M8V0A1 + ON + false + 2015-01-22T23:17:26.681Z + + INO + INTERLINE OUTBOUND + COLIS REMIS ¿ UNE FIRME DE TRANSPORT DE LIAISON + + 20110307 230559 + 3 + + + + + JOHN CYOPECK CENTRE + 205 NEW TORONTO STREET + + + ETOBICOKE + CA + + + -1 + 2015-01-22T23:17:26.681Z + + + M8V0A1 + ON + false + 2015-01-22T23:17:26.681Z + + ARR + ARRIVAL AT HUB/TERMINAL FROM BULK/CITY TRAILER + COLIS ARRIV… AU CENTRE PAR REMORQUE DE TRANSPORT EN VRAC OU LOCAL + + 20110307 230314 + 3 + + + + + JOHN CYOPECK CENTRE + 205 NEW TORONTO STREET + + + ETOBICOKE + CA + + + -1 + 2015-01-22T23:17:26.681Z + + + M8V0A1 + ON + false + 2015-01-22T23:17:26.681Z + + PIC + PICKUP FROM CUSTOMER/SHIPPER + CUEILLETTE EFFECTU…E PAR LE CLIENT, L'EXP…DITEUR. + + 20110307 145353 + 3 + + + GROUND + GROUND (fr) + 20110307 + + + http://www.canpar.com/en/track/TrackingAction.do?locale=en&type=0&reference=D999999988030400000008 + http://www.canpar.com/fr/track/TrackingAction.do?locale=fr&type=0&reference=D999999988030400000008 + + + + + + +""" diff --git a/tests/dhl/package/address_validation.py b/tests/dhl/package/address_validation.py deleted file mode 100644 index 1e965e077b..0000000000 --- a/tests/dhl/package/address_validation.py +++ /dev/null @@ -1,105 +0,0 @@ -import re -import unittest -from unittest.mock import patch -from purplship.core.utils.helpers import to_dict -from purplship.core.models import AddressValidationRequest -from purplship.package import Address -from tests.dhl.package.fixture import gateway - - -class TestDHLAddressValidation(unittest.TestCase): - def setUp(self): - self.maxDiff = None - self.AddressValidationRequest = AddressValidationRequest( - **AddressValidationPayload - ) - - def test_create_address_validation_request(self): - request = gateway.mapper.create_address_validation_request( - self.AddressValidationRequest - ) - # remove MessageTime, Date and ReadyTime for testing purpose - serialized_request = re.sub( - "[^>]+", "", request.serialize() - ) - self.assertEqual( - serialized_request, - AddressValidationRequestXML, - ) - - @patch("purplship.package.mappers.dhl.proxy.http", return_value="") - def test_validated_address(self, http_mock): - Address.validate(self.AddressValidationRequest).from_(gateway) - - url = http_mock.call_args[1]["url"] - self.assertEqual(url, gateway.settings.server_url) - - def test_parse_address_validation_response(self): - with patch("purplship.package.mappers.dhl.proxy.http") as mock: - mock.return_value = AddressValidationResponseXML - parsed_response = ( - Address.validate(self.AddressValidationRequest).from_(gateway).parse() - ) - - self.assertEqual( - to_dict(parsed_response), to_dict(ParsedAddressValidationResponse) - ) - - -if __name__ == "__main__": - unittest.main() - - -AddressValidationPayload = {} - -ParsedAddressValidationResponse = {} - - -AddressValidationRequestXML = """ - - - - 1234567890123456789012345678901 - - - - - 3PV - 1.0 - - - AM - O - Suit 333 - 333 Twin94089 - North Dakhota - California - US - United States of America - US - -""" - -AddressValidationResponseXML = """ - - - - 2018-02-21T04:01:48+01:00 - Routing_Request_Global_AM_v62__ - CustomerSiteID - - - - Success - - Y - 08:00 - AM - - NUQ - NUQ|0|GMT-8:00|FREMONT,CA-USA|US - - - -""" diff --git a/tests/dhl_express/address.py b/tests/dhl_express/address.py index 33fb3a73b9..b011dac1ce 100644 --- a/tests/dhl_express/address.py +++ b/tests/dhl_express/address.py @@ -62,7 +62,11 @@ def test_parse_address_validation_response(self): } ParsedAddressValidationResponse = [ - {"carrier_id": "carrier_id", "carrier_name": "dhl_express", "success": True}, + { + "carrier_id": "carrier_id", + "carrier_name": "dhl_express", + "success": True, + }, [], ] diff --git a/tests/dhl_express/pickup.py b/tests/dhl_express/pickup.py index 7bca1abcc4..70439c89c7 100644 --- a/tests/dhl_express/pickup.py +++ b/tests/dhl_express/pickup.py @@ -3,7 +3,7 @@ from unittest.mock import patch from purplship.core.utils.helpers import to_dict from purplship.core.models import ( - PickupCancellationRequest, + PickupCancelRequest, PickupRequest, PickupUpdateRequest, ) @@ -16,7 +16,7 @@ def setUp(self): self.maxDiff = None self.BookPURequest = PickupRequest(**book_pickup_payload) self.ModifyPURequest = PickupUpdateRequest(**modification_data) - self.CancelPURequest = PickupCancellationRequest(**cancellation_data) + self.CancelPURequest = PickupCancelRequest(**cancellation_data) def test_create_pickup_request(self): request = gateway.mapper.create_pickup_request(self.BookPURequest) @@ -28,7 +28,7 @@ def test_create_pickup_request(self): self.assertEqual(serialized_request, PickupRequestXML) def test_create_modify_pickup_request(self): - request = gateway.mapper.create_modify_pickup_request(self.ModifyPURequest) + request = gateway.mapper.create_pickup_update_request(self.ModifyPURequest) # remove MessageTime for testing purpose serialized_request = re.sub( "[^>]+", "", request.serialize() @@ -50,7 +50,7 @@ def test_create_pickup_cancellation_request(self): def test_parse_request_pickup_response(self): with patch("purplship.mappers.dhl_express.proxy.http") as mock: mock.return_value = PickupResponseXML - parsed_response = Pickup.book(self.BookPURequest).with_(gateway).parse() + parsed_response = Pickup.schedule(self.BookPURequest).with_(gateway).parse() self.assertEqual(to_dict(parsed_response), to_dict(ParsedPickupResponse)) @@ -71,7 +71,7 @@ def test_parse_cancellation_pickup_response(self): def test_parse_request_pickup_error(self): with patch("purplship.mappers.dhl_express.proxy.http") as mock: mock.return_value = PickupErrorResponseXML - parsed_response = Pickup.book(self.BookPURequest).with_(gateway).parse() + parsed_response = Pickup.schedule(self.BookPURequest).with_(gateway).parse() self.assertEqual( to_dict(parsed_response), to_dict(ParsedPickupErrorResponse) @@ -82,7 +82,7 @@ def test_parse_request_pickup_error(self): unittest.main() book_pickup_payload = { - "date": "2013-10-19", + "pickup_date": "2013-10-19", "ready_time": "10:20", "closing_time": "09:20", "instruction": "behind the front desk", @@ -100,7 +100,7 @@ def test_parse_request_pickup_error(self): } modification_data = { - "date": "2013-10-19", + "pickup_date": "2013-10-19", "confirmation_number": "100094", "ready_time": "10:20", "closing_time": "09:20", @@ -117,9 +117,11 @@ def test_parse_request_pickup_error(self): cancellation_data = { "confirmation_number": "743511", - "person_name": "Rikhil", "pickup_date": "2013-10-10", - "country_code": "BR", + "address": { + "person_name": "Rikhil", + "country_code": "BR", + }, } ParsedPickupResponse = [ @@ -155,7 +157,12 @@ def test_parse_request_pickup_error(self): ] ParsedCancelPUResponse = [ - {"carrier_id": "carrier_id", "carrier_name": "dhl_express", "success": True}, + { + "carrier_id": "carrier_id", + "carrier_name": "dhl_express", + "operation": "Cancel Pickup", + "success": True, + }, [], ] diff --git a/tests/dhl_express/shipment.py b/tests/dhl_express/shipment.py index db753a9885..6a6ca08cdc 100644 --- a/tests/dhl_express/shipment.py +++ b/tests/dhl_express/shipment.py @@ -103,7 +103,7 @@ def test_parse_shipment_response(self): "payment": {"paid_by": "sender", "account_number": "123456789"}, "customs": { "commodities": [{"description": "cn", "sku": "cc"}], - "terms_of_trade": "DAP", + "incoterm": "DAP", "duty": {"account_number": "123456789", "paid_by": "sender", "amount": 200.00}, }, "doc_images": [ @@ -145,6 +145,7 @@ def test_parse_shipment_response(self): "carrier_id": "carrier_id", "label": "JVBERi0xLjQKJfbk/N8KMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovVmVyc2lvbiAvMS40Ci9Q\nYWdlcyAyIDAgUgo+PgplbmRvYmoKMyAwIG9iago8PAovUHJvZHVjZXIgKGlUZXh0IDIuMS43IGJ5\nIDFUM1hUKQovTW9kRGF0ZSAoRDoyMDE4MDIyNzA4MDg0OSswMScwMCcpCi9DcmVhdGlvbkRhdGUg\nKEQ6MjAxODAyMjcwODA4NDkrMDEnMDAnKQo+PgplbmRvYmoKMiAwIG9iago8PAovVHlwZSAvUGFn\nZXMKL0tpZHMgWzQgMCBSIDUgMCBSXQovQ291bnQgMgo+PgplbmRvYmoKNCAwIG9iago8PAovUGFy\nZW50IDIgMCBSCi9Db250ZW50cyA2IDAgUgovVHlwZSAvUGFnZQovUmVzb3VyY2VzIDcgMCBSCi9N\nZWRpYUJveCBbMC4wIDAuMCA4NDEuODkgNTk1LjI4XQovQ3JvcEJveCBbMC4wIDAuMCA4NDEuODkg\nNTk1LjI4XQovUm90YXRlIDAKPj4KZW5kb2JqCjUgMCBvYmoKPDwKL1BhcmVudCAyIDAgUgovQ29u\ndGVudHMgOCAwIFIKL1R5cGUgL1BhZ2UKL1Jlc291cmNlcyA5IDAgUgovTWVkaWFCb3ggWzAuMCAw\nLjAgODQxLjg5IDU5NS4yOF0KL0Nyb3BCb3ggWzAuMCAwLjAgODQxLjg5IDU5NS4yOF0KL1JvdGF0\nZSAwCj4+CmVuZG9iago2IDAgb2JqCjw8Ci9MZW5ndGggNTEKL0ZpbHRlciAvRmxhdGVEZWNvZGUK\nPj4Kc3RyZWFtDQp4nCvkcgrhMlAwtTTVM7JQCEnhcg3hCuQqVDBUMABCCJmcq6AfkWao4JKvEMgF\nAP2hClYNCmVuZHN0cmVhbQplbmRvYmoKNyAwIG9iago8PAovWE9iamVjdCA8PAovWGYxIDEwIDAg\nUgo+PgovUHJvY1NldCBbL1BERiAvVGV4dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSV0KPj4KZW5k\nb2JqCjggMCBvYmoKPDwKL0xlbmd0aCA1MQovRmlsdGVyIC9GbGF0ZURlY29kZQo+PgpzdHJlYW0N\nCnicK+RyCuEyUDC1NNUzslAISeFyDeEK5CpUMFQwAEIImZyroB+RZqjgkq8QyAUA/aEKVg0KZW5k\nc3RyZWFtCmVuZG9iago5IDAgb2JqCjw8Ci9YT2JqZWN0IDw8Ci9YZjEgMTEgMCBSCj4+Ci9Qcm9j\nU2V0IFsvUERGIC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJXQo+PgplbmRvYmoKMTAgMCBv\nYmoKPDwKL0xlbmd0aCA0NDgxCi9UeXBlIC9YT2JqZWN0Ci9SZXNvdXJjZXMgPDwKL1hPYmplY3Qg\nPDwKL2ltZzAgMTIgMCBSCj4+Ci9Qcm9jU2V0IFsvUERGIC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAv\nSW1hZ2VJXQovRm9udCAxMyAwIFIKPj4KL1N1YnR5cGUgL0Zvcm0KL0JCb3ggWzAgMCA4NDEuODkg\nNTk1LjI4XQovTWF0cml4IFsxIDAgMCAxIDAgMF0KL0Zvcm1UeXBlIDEKL0ZpbHRlciAvRmxhdGVE\nZWNvZGUKPj4Kc3RyZWFtDQp4nJWbW3fcxpWF3/kr8GhnRhDqXtCbbs6SR7YZiY6cNZ4HDtUSO0az\n5WY7Gv37OZdC1a5WK8uK1kq4w7M/oC44daoA/n4xDWEOo83Djn6chuUiezPmGX8sAcvF7cWbi7sL\nM3y8sMP3FP7PCzMNP1z89/9Mw9uL38U/DYf3F0+uLh5+ZwZjRuOHq3fk4F+YYc6Cc2lMYbjaXTyY\nRmvjnIarm4tvnv9y+er569fDm59evXz25sWz599e/ZOQ9KvnV0q0gx+DQaCx0+j9EGwep5mJ0+hC\nMFaAdjL5wWQf2DT88sPLyxdDHO3wcPjLPD0wcfJ/Qb7c+jhNbrbcAGs932IIdkyResYmunJa9VK1\njWPwrEv8qm8v+B75X+sNa8fg8OatDeNEJjcxlLvDjFOUW39z+ctndyf3NVOIG4KnATF8XzlzjwQa\npzTTdd1k+RednlfJIquiG2EVgqgpDTfNOo3R0G/NGKlj3TxmEXYW4ZyE0sAa0RP/dmJ6cHmMQWJz\nWBXHptHHVS8n2k15dFEvw6RZuHQLM1vNxPMneDs6+Z1dG+6mMIaqbk66ZaHOtzmNfBee75D6abY8\nMEKW8eKZaFDPHK6ShZ+1J7Ioo7+zVq6lVrplGjVW1klzZlaJ28HKZ4mlmS7SevmlyzL7yc9Gnrmq\nuJ/imN2qF9HceBe5O93kxpzW9vAtyS2Iuukby613Jo7erLAdaTfG1CIY7gNqauPcLs7T11kaqLQO\n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\nv3PKit1gq65+DUd/7EabkiKOtshmjv1o89GUR3OAViyrrnYNRz/O0qS1WTOLbuZujmb5TLOZKZfi\naIusVg1Gt+v7LOvBZrOLbn530me0zcmdf+46TWRzSzC4KW92Yz0H/ryq2VVXv4ajf+7azpuNCKmh\n6Oaf+9bzXgLvnvcaxqNf9Goo4eiXP/kC/wxzf1l180s4+M3UzRreaGD7i65+DUd/5L9jaX4rf8bT\n/KqbX8LBz38xidfn/Nf5RVe/hqM/y/FC9XNGRL/q5pdw8OvndOAP0J5l1dWv4eiPsqFtft1gNL/o\n5pfw5v/ydiPK9y2UMEL9Y7Kp7ja+//Xb4ftnE3/VLX9azJewdh6iSzNuO/5G//4fvwaz6g0KZW5k\nc3RyZWFtCmVuZG9iagoxMSAwIG9iago8PAovTGVuZ3RoIDMxOTgKL1R5cGUgL1hPYmplY3QKL1Jl\nc291cmNlcyA8PAovWE9iamVjdCA8PAovaW1nMCAxNCAwIFIKPj4KL1Byb2NTZXQgWy9QREYgL1Rl\neHQgL0ltYWdlQiAvSW1hZ2VDIC9JbWFnZUldCi9Gb250IDE1IDAgUgo+PgovU3VidHlwZSAvRm9y\nbQovQkJveCBbMCAwIDg0MS44OSA1OTUuMjhdCi9NYXRyaXggWzEgMCAwIDEgMCAwXQovRm9ybVR5\ncGUgMQovRmlsdGVyIC9GbGF0ZURlY29kZQo+PgpzdHJlYW0NCniclZrbchs3Eobv+RSo2hsntRrj\nfPCdTs4669iKJEdJJblgqJHEmOQoQypav/12NzCcBkWnpHLVrn+z8eHQQHcDk78mUrjkGh3FEv4q\nxWISrWpi4n8tBovJ3eRqspoo8TjR4nsw/3OipPhh8uvvUlxP/qL2UvS3k6PLyeu3SijV6CAub6AF\n/oD/IJtkhTOpUUZcQo+NCdZ7cTmbvPpWHJ4f/+fdT6fi5OOx+Pabyz8BCD+cXmaeFr4J/gkuCKdj\nE1TBqSAj4T50G7HpxB+tmG4209lde43yfjr7PL1tOZxG3Uhpksaxa22b4IRzGntbTnSwjQuDXmy1\n9o2zqIv9oO8mOED8My6E1o0zfORau0ZCIyNpSsvJgWpkXoars5+fjI7GlcDECGfBFwrHFWPjFGjV\nhAT9Gqnxh0qnQaKIWcFAUDlHSgYxG5vKxiv4VTU+OymS0ImEMWSqGqVIS/xVIt2Z2HhHttENCm1D\n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\nl13lzfa7J+Sd8t3z/XzWrvCr2YI+03Y34p5ewsR8Rf3ja92zH4/x0laWxcSQ8oPO9ycSrvXGOxPh\ngqKTN+E5D9xY5EByhmRlyg6wMbjsuANxP71twQiGq8QBp/0If/4PWx5djw0KZW5kc3RyZWFtCmVu\nZG9iagoxMiAwIG9iago8PAovTGVuZ3RoIDcxNTUKL1R5cGUgL1hPYmplY3QKL1N1YnR5cGUgL0lt\nYWdlCi9Db2xvclNwYWNlIFsvSW5kZXhlZCAvRGV2aWNlUkdCIDI1NSA8RDJFNTlDQkNENzZCRTlG\nMkNFRkFGQ0YzQUJDRDQ1QzdERTg0RERFQkI1RjRGOUU3QjZENDVFQjBEMTUyRUVGNURCRDhFOEE5\nQ0RFMTkwRTNFRkMyQzFEQjc3QkJENzZBQUFDRDQ1RThGMUNERjlGQkYyQzZERDgzQjVEMzVFRjNG\nOEU2RUVGNUQ5RDJFNDlDRERFQkI0QzFEQTc2RDdFN0E4QjBEMDUxQ0NFMThGRTJFRUMxQTVDQTM5\nRkZGRkZGMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwPl0KL1dpZHRoIDEwMjQKL0Jp\ndHNQZXJDb21wb25lbnQgOAovSGVpZ2h0IDc2OAovRmlsdGVyIC9GbGF0ZURlY29kZQo+PgpzdHJl\n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\neGVkIC9EZXZpY2VSR0IgMjU1IDxEMkU1OUNCQ0Q3NkJFOUYyQ0VGQUZDRjNBQkNENDVDN0RFODRE\nREVCQjVGNEY5RTdCNkQ0NUVCMEQxNTJFRUY1REJEOEU4QTlDREUxOTBFM0VGQzJDMURCNzdCQkQ3\nNkFBQUNENDVFOEYxQ0RGOUZCRjJDNkREODNCNUQzNUVGM0Y4RTZFRUY1RDlEMkU0OUNEREVCQjRD\nMURBNzZEN0U3QThCMEQwNTFDQ0UxOEZFMkVFQzFBNUNBMzlGRkZGRkYwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\nMDAwMDAwMDAwMDAwMDAwMDA+XQovV2lkdGggMTAyNAovQml0c1BlckNvbXBvbmVudCA4Ci9IZWln\naHQgNzY4Ci9GaWx0ZXIgL0ZsYXRlRGVjb2RlCj4+CnN0cmVhbQ0KeJzt3VlD48a2gFFPYIyBJJ3x\nDKH//7+8cAi3sZGNLe0qqbbWesyD4vbmkzWU5e/fAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACASVst9vdjv4Zito+Lx/XYLwKm6nH3\n/GKZcw+wfnj9xz0/jP06YJpun99sUu4Alv/8657GfiEwRevNP4U8L8d+KQW879yen2/HfikwQT8K\n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\nNja9+36vIvYqRL+DEJihTWh6PV9E7FUIl//hQk+R5fW7/P/9+23ki3heh75BkFhoevueL2Id+SJc\n/oNLhV4A2PZ9FXeBL6LvTghmKDC9Xe8XEbkCqPdOCOYnML3+n7yBRyEO/+FygefeAz554y5D9lyC\nAPN0E1XezYAX8Rj1Ijau/sMVwo69B9133wW9CIt/4CpBBwB9b/6/CboP6eMfrhN0ADBw2V3MAYCP\nf7hSyAHAkLP/VyHfRPLxD9daB3wJYDP4tnvELQAX/+FqAVffh6+62w7fCw27BAEzNfizN6K8wZcA\nhx+DwBytB159iznvHnod4jHiRcD8DHwEV8xX7tfDvovwEPIiYIYGHXxHXXYbdAnAyT/0NuB7QENv\n/f1w338HcOfWH/TX++w7Lv8BOwD5wyA9n8IXmX/vHYD8YaBe1wCiV9z22gHIHwZ7vLq9TfyKu+31\ndwFiD0Fgpu6vbO+u5wP/z1pfeSWiwD4I5umqiwAPhQ67rzoOKbIPgnlaXXwIsCv3QxuXHwJsfOMX\nIt1e9OlbOLzVZb8JdmPJP8RaL77cA2wWxa+4X7AHUD+UcHv2LOCuzhW31dmzgN1C/VDIdn9iF3D3\nUK+79e2JbyZvbnzZD4paPz4cHYMvH26rf+iuFsvDbyff3exd8ocq1qvVfvFiv1qNuMhutbp9fRGL\n1Ur6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAJDa/wHh82BSDQplbmRzdHJlYW0KZW5kb2JqCjE1IDAgb2JqCjw8Ci9GMSAxOCAwIFIKL0Yy\nIDE5IDAgUgo+PgplbmRvYmoKMTYgMCBvYmoKPDwKL0Jhc2VGb250IC9IZWx2ZXRpY2EtQm9sZAov\nVHlwZSAvRm9udAovRW5jb2RpbmcgL1dpbkFuc2lFbmNvZGluZwovU3VidHlwZSAvVHlwZTEKPj4K\nZW5kb2JqCjE3IDAgb2JqCjw8Ci9CYXNlRm9udCAvSGVsdmV0aWNhCi9UeXBlIC9Gb250Ci9FbmNv\nZGluZyAvV2luQW5zaUVuY29kaW5nCi9TdWJ0eXBlIC9UeXBlMQo+PgplbmRvYmoKMTggMCBvYmoK\nPDwKL0Jhc2VGb250IC9IZWx2ZXRpY2EtQm9sZAovVHlwZSAvRm9udAovRW5jb2RpbmcgL1dpbkFu\nc2lFbmNvZGluZwovU3VidHlwZSAvVHlwZTEKPj4KZW5kb2JqCjE5IDAgb2JqCjw8Ci9CYXNlRm9u\ndCAvSGVsdmV0aWNhCi9UeXBlIC9Gb250Ci9FbmNvZGluZyAvV2luQW5zaUVuY29kaW5nCi9TdWJ0\neXBlIC9UeXBlMQo+PgplbmRvYmoKeHJlZgowIDIwCjAwMDAwMDAwMDAgNjU1MzUgZg0KMDAwMDAw\nMDAxNSAwMDAwMCBuDQowMDAwMDAwMjA3IDAwMDAwIG4NCjAwMDAwMDAwNzggMDAwMDAgbg0KMDAw\nMDAwMDI3MCAwMDAwMCBuDQowMDAwMDAwNDI3IDAwMDAwIG4NCjAwMDAwMDA1ODQgMDAwMDAgbg0K\nMDAwMDAwMDcwOCAwMDAwMCBuDQowMDAwMDAwODAyIDAwMDAwIG4NCjAwMDAwMDA5MjYgMDAwMDAg\nbg0KMDAwMDAwMTAyMCAwMDAwMCBuDQowMDAwMDA1NzcxIDAwMDAwIG4NCjAwMDAwMDkyMzkgMDAw\nMDAgbg0KMDAwMDAxODEyMiAwMDAwMCBuDQowMDAwMDE4MTY2IDAwMDAwIG4NCjAwMDAwMjcwNDkg\nMDAwMDAgbg0KMDAwMDAyNzA5MyAwMDAwMCBuDQowMDAwMDI3MTk2IDAwMDAwIG4NCjAwMDAwMjcy\nOTQgMDAwMDAgbg0KMDAwMDAyNzM5NyAwMDAwMCBuDQp0cmFpbGVyCjw8Ci9Sb290IDEgMCBSCi9J\nbmZvIDMgMCBSCi9JRCBbPDcxMjU2RkRCMTNFNzhCQUQ3QkM2RjBGOUZBRDQ1NjFEPiA8NzEyNTZG\nREIxM0U3OEJBRDdCQzZGMEY5RkFENDU2MUQ+XQovU2l6ZSAyMAo+PgpzdGFydHhyZWYKMjc0OTUK\nJSVFT0YK\n", "tracking_number": "0057714403", + "shipment_identifier": "0057714403", }, [], ] diff --git a/tests/fedex_express/address.py b/tests/fedex_express/address.py index 4794ce737c..b180752338 100644 --- a/tests/fedex_express/address.py +++ b/tests/fedex_express/address.py @@ -89,7 +89,7 @@ def test_parse_address_validation_response(self): 1293587 - AddressValidationRequest_v2 + AddressValidationRequest_v4 aval diff --git a/tests/fedex_express/pickup.py b/tests/fedex_express/pickup.py index 2b6619ed5c..f48c04fef8 100644 --- a/tests/fedex_express/pickup.py +++ b/tests/fedex_express/pickup.py @@ -6,7 +6,7 @@ from purplship.core.models import ( PickupRequest, PickupUpdateRequest, - PickupCancellationRequest, + PickupCancelRequest, ) from tests.fedex_express.fixture import gateway @@ -18,7 +18,7 @@ def setUp(self): self.maxDiff = None self.PickupRequest = PickupRequest(**pickup_data) self.PickupUpdateRequest = PickupUpdateRequest(**pickup_update_data) - self.PickupCancelRequest = PickupCancellationRequest(**pickup_cancel_data) + self.PickupCancelRequest = PickupCancelRequest(**pickup_cancel_data) def test_create_pickup_request(self): request = gateway.mapper.create_pickup_request(self.PickupRequest) @@ -32,7 +32,7 @@ def test_create_pickup_request(self): self.assertEqual(create_pickup_request.data.serialize(), PickupRequestXML) def test_update_pickup_request(self): - request = gateway.mapper.create_modify_pickup_request(self.PickupUpdateRequest) + request = gateway.mapper.create_pickup_update_request(self.PickupUpdateRequest) pipeline = request.serialize() get_availability_request = pipeline["get_availability"]() create_pickup_request = pipeline["create_pickup"](PickupAvailabilityResponseXML) @@ -48,7 +48,7 @@ def test_update_pickup_request(self): def test_create_pickup(self): with patch("purplship.mappers.fedex_express.proxy.http") as mocks: mocks.side_effect = [PickupAvailabilityResponseXML, PickupResponseXML] - purplship.Pickup.book(self.PickupRequest).with_(gateway) + purplship.Pickup.schedule(self.PickupRequest).with_(gateway) availability_call, create_call = mocks.call_args_list self.assertEqual( @@ -87,7 +87,7 @@ def test_parse_pickup_reply(self): with patch("purplship.mappers.fedex_express.proxy.http") as mocks: mocks.side_effect = [PickupAvailabilityResponseXML, PickupResponseXML] parsed_response = ( - purplship.Pickup.book(self.PickupRequest).with_(gateway).parse() + purplship.Pickup.schedule(self.PickupRequest).with_(gateway).parse() ) self.assertListEqual(to_dict(parsed_response), ParsedPickupResponse) @@ -106,7 +106,7 @@ def test_parse_pickup_cancel_reply(self): unittest.main() pickup_data = { - "date": "2015-01-28", + "pickup_date": "2015-01-28", "address": { "company_name": "Jim Duggan", "address_line1": "2271 Herring Cove", @@ -126,7 +126,7 @@ def test_parse_pickup_cancel_reply(self): pickup_update_data = { "confirmation_number": "0074698052", - "date": "2015-01-28", + "pickup_date": "2015-01-28", "address": { "person_name": "Jane Doe", "email": "john.doe@canadapost.ca", @@ -150,7 +150,12 @@ def test_parse_pickup_cancel_reply(self): ] ParsedPickupCancelResponse = [ - {"carrier_id": "carrier_id", "carrier_name": "fedex_express", "success": True}, + { + "carrier_id": "carrier_id", + "carrier_name": "fedex_express", + "operation": "Cancel Pickup", + "success": True, + }, [], ] diff --git a/tests/fedex_express/shipment.py b/tests/fedex_express/shipment.py index 5fc6579e95..a23322cd56 100644 --- a/tests/fedex_express/shipment.py +++ b/tests/fedex_express/shipment.py @@ -3,7 +3,7 @@ import logging from unittest.mock import patch from purplship.core.utils.helpers import to_dict -from purplship.core.models import ShipmentRequest +from purplship.core.models import ShipmentRequest, ShipmentCancelRequest from purplship import Shipment from tests.fedex_express.fixture import gateway @@ -14,6 +14,7 @@ class TestFedExShipment(unittest.TestCase): def setUp(self): self.maxDiff = None self.ShipmentRequest = ShipmentRequest(**shipment_data) + self.ShipmentCancelRequest = ShipmentCancelRequest(**shipment_cancel_data) def test_create_shipment_request(self): request = gateway.mapper.create_shipment_request(self.ShipmentRequest) @@ -24,6 +25,13 @@ def test_create_shipment_request(self): self.assertEqual(serialized_request, ShipmentRequestXml) + def test_create_cancel_shipment_request(self): + request = gateway.mapper.create_cancel_shipment_request( + self.ShipmentCancelRequest + ) + + self.assertEqual(request.serialize(), ShipmentCancelRequestXML) + @patch("purplship.mappers.fedex_express.proxy.http", return_value="") def test_create_shipment(self, http_mock): Shipment.create(self.ShipmentRequest).with_(gateway) @@ -31,6 +39,13 @@ def test_create_shipment(self, http_mock): url = http_mock.call_args[1]["url"] self.assertEqual(url, f"{gateway.settings.server_url}/ship") + @patch("purplship.mappers.fedex_express.proxy.http", return_value="") + def test_cancel_shipment(self, http_mock): + Shipment.cancel(self.ShipmentCancelRequest).from_(gateway) + + url = http_mock.call_args[1]["url"] + self.assertEqual(url, f"{gateway.settings.server_url}/ship") + def test_parse_shipment_response(self): with patch("purplship.mappers.fedex_express.proxy.http") as mock: mock.return_value = ShipmentResponseXML @@ -40,6 +55,17 @@ def test_parse_shipment_response(self): self.assertEqual(to_dict(parsed_response), to_dict(ParsedShipmentResponse)) + def test_parse_shipment_cancel_response(self): + with patch("purplship.mappers.fedex_express.proxy.http") as mock: + mock.return_value = ShipmentResponseXML + parsed_response = ( + Shipment.cancel(self.ShipmentCancelRequest).from_(gateway).parse() + ) + + self.assertEqual( + to_dict(parsed_response), to_dict(ParsedShipmentCancelResponse) + ) + if __name__ == "__main__": unittest.main() @@ -86,16 +112,33 @@ def test_parse_shipment_response(self): "customs": {"duty": {"paid_by": "sender", "amount": "100."}}, } +shipment_cancel_data = { + "shipment_identifier": "794947717776", + "service": "fedex_express", +} + ParsedShipmentResponse = [ { "carrier_name": "fedex_express", "carrier_id": "carrier_id", "label": "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\nfQ13fd6yP/7wsz/+7mebv3v7L/+yUh7+9ubd/fp/PbXeIN7N19nOeV85n7168u9f1/HC39yd39+h\n53tw/mf3D+9O4M55z/7Zqz//j6++/PzLet6bL7D8/uv7h/Cnu+b97atf/+mrv3v7w89/9uX/rOV9\nx3jf/WT3vP/0T19997Na3rvf3AU/Oae83zyc/7dd88avnnxn/eSjP3zy0feuKnm/QrwB5v3V17+7\nP/8vo/MWXsn6L54he3HxrY/+VeU6jfvPb4J/A7/4Gv78y69/+Op8sWve1kj70Av+rXX0wrq8fuee\nBZVXGu0T787934l34p14J96Jd+KdeCfeiXfinXgn3ol34p14J96Jd+KdeCfeiXfinXgn3ol34p14\nJ96Jd+KdeCfeiXfinXgn3ol34p14J96Jd+KdeCfeiXfinXgn3ol34p14J96Jd+KdeCfeiXfinXgn\n3ol34p14J96Jd+KdeCfeiXfinXgn3ol34p14J96Jd+KdeCfeiXfinXgn3ol34p14J96Jd+KdeCfe\niXfinXgn3ol34p14J96Jd+KdeCde/vq/IYsam1SzVhkAAAAASUVORK5CYII=\n", "tracking_number": "794947717776", + "shipment_identifier": "794947717776", }, [], ] +ParsedShipmentCancelResponse = [ + { + "carrier_id": "carrier_id", + "carrier_name": "fedex_express", + "operation": "Cancel Shipment", + "success": True, + }, + [], +] + + ShipmentRequestXml = """ @@ -698,3 +741,61 @@ def test_parse_shipment_response(self): """ + +ShipmentCancelRequestXML = """ + + + + + user_key + password + + + + 2349857 + 1293587 + + + Delete Shipment + + + ship + 23 + 0 + 0 + + + EXPRESS + 794947717776 + + DELETE_ALL_PACKAGES + + + +""" + +ShipmentCancelResponseXML = """ + + + + SUCCESS + + SUCCESS + ship + 0000 + Success + Success + + + DeleteShipmentRequest_v25 + + + ship + 25 + 0 + 0 + + + + +""" diff --git a/tests/purolator_courier/pickup.py b/tests/purolator_courier/pickup.py index 111eb3883d..58841e2460 100644 --- a/tests/purolator_courier/pickup.py +++ b/tests/purolator_courier/pickup.py @@ -6,7 +6,7 @@ from purplship.core.models import ( PickupRequest, PickupUpdateRequest, - PickupCancellationRequest, + PickupCancelRequest, ) from tests.purolator_courier.fixture import gateway @@ -18,7 +18,7 @@ def setUp(self): self.maxDiff = None self.PickupRequest = PickupRequest(**pickup_data) self.PickupUpdateRequest = PickupUpdateRequest(**pickup_update_data) - self.PickupCancelRequest = PickupCancellationRequest(**pickup_cancel_data) + self.PickupCancelRequest = PickupCancelRequest(**pickup_cancel_data) def test_create_pickup_request(self): request = gateway.mapper.create_pickup_request(self.PickupRequest) @@ -30,7 +30,7 @@ def test_create_pickup_request(self): self.assertEqual(schedule_request.data.serialize(), PickupRequestXML) def test_update_pickup_request(self): - request = gateway.mapper.create_modify_pickup_request(self.PickupUpdateRequest) + request = gateway.mapper.create_pickup_update_request(self.PickupUpdateRequest) pipeline = request.serialize() validate_request = pipeline["validate"]() modify_request = pipeline["modify"](PickupValidationResponseXML) @@ -43,7 +43,7 @@ def test_update_pickup_request(self): def test_create_pickup(self): with patch("purplship.mappers.purolator_courier.proxy.http") as mocks: mocks.side_effect = [PickupValidationResponseXML, PickupResponseXML] - purplship.Pickup.book(self.PickupRequest).with_(gateway) + purplship.Pickup.schedule(self.PickupRequest).with_(gateway) validate_call, schedule_call = mocks.call_args_list self.assertEqual( @@ -94,7 +94,7 @@ def test_parse_pickup_reply(self): with patch("purplship.mappers.purolator_courier.proxy.http") as mocks: mocks.side_effect = [PickupValidationResponseXML, PickupResponseXML] parsed_response = ( - purplship.Pickup.book(self.PickupRequest).with_(gateway).parse() + purplship.Pickup.schedule(self.PickupRequest).with_(gateway).parse() ) self.assertListEqual(to_dict(parsed_response), ParsedPickupResponse) @@ -113,7 +113,7 @@ def test_parse_pickup_cancel_reply(self): unittest.main() pickup_data = { - "date": "2015-01-28", + "pickup_date": "2015-01-28", "address": { "company_name": "Jim Duggan", "address_line1": "2271 Herring Cove", @@ -133,7 +133,7 @@ def test_parse_pickup_cancel_reply(self): pickup_update_data = { "confirmation_number": "0074698052", - "date": "2015-01-28", + "pickup_date": "2015-01-28", "address": { "person_name": "Jane Doe", "email": "john.doe@canadapost.ca", @@ -161,6 +161,7 @@ def test_parse_pickup_cancel_reply(self): { "carrier_id": "purolator_courier", "carrier_name": "purolator_courier", + "operation": "Cancel Pickup", "success": True, }, [], diff --git a/tests/purolator_courier/shipment.py b/tests/purolator_courier/shipment.py index 8dad323df6..fabd56a987 100644 --- a/tests/purolator_courier/shipment.py +++ b/tests/purolator_courier/shipment.py @@ -1,9 +1,10 @@ import re import unittest +import logging from datetime import datetime from unittest.mock import patch from purplship.core.utils.helpers import to_dict -from purplship.core.models import ShipmentRequest +from purplship.core.models import ShipmentRequest, ShipmentCancelRequest from purplship import Shipment from tests.purolator_courier.fixture import gateway @@ -12,6 +13,9 @@ class TestPurolatorShipment(unittest.TestCase): def setUp(self): self.maxDiff = None self.ShipmentRequest = ShipmentRequest(**SHIPMENT_REQUEST_PAYLOAD) + self.ShipmentCancelRequest = ShipmentCancelRequest( + **SHIPMENT_CANCEL_REQUEST_PAYLOAD + ) def test_create_shipment_request(self): request = gateway.mapper.create_shipment_request(self.ShipmentRequest) @@ -29,7 +33,14 @@ def test_create_shipment_request(self): document_request.data.serialize(), SHIPMENT_DOCUMENT_REQUEST_XML ) - def test_send_valid_shipment(self): + def test_cancel_shipment_request(self): + request = gateway.mapper.create_cancel_shipment_request( + self.ShipmentCancelRequest + ) + + self.assertEqual(request.serialize(), SHIPMENT_CANCEL_REQUEST_XML) + + def test_request_shipment(self): with patch("purplship.mappers.purolator_courier.proxy.http") as mocks: mocks.side_effect = [ VALIDATE_SHIPMENT_RESPONSE_XML, @@ -53,6 +64,17 @@ def test_send_valid_shipment(self): f"{gateway.settings.server_url}/EWS/V1/ShippingDocuments/ShippingDocumentsService.asmx", ) + def test_cancel_shipment(self): + with patch("purplship.mappers.purolator_courier.proxy.http") as mock: + mock.return_value = "" + Shipment.cancel(self.ShipmentCancelRequest).from_(gateway) + url = mock.call_args[1]["url"] + + self.assertEqual( + url, + f"{gateway.settings.server_url}/EWS/V2/Shipping/ShippingService.asmx", + ) + def test_parse_shipment_response(self): with patch("purplship.mappers.purolator_courier.proxy.http") as mocks: mocks.side_effect = [ @@ -78,10 +100,23 @@ def test_parse_invalid_shipment_response(self): to_dict(parsed_response), to_dict(PARSED_INVALID_SHIPMENT_RESPONSE) ) + def test_parse_cancel_shipment_response(self): + with patch("purplship.mappers.purolator_courier.proxy.http") as mocks: + mocks.return_value = SHIPMENT_CANCEL_RESPONSE_XML + parsed_response = ( + Shipment.cancel(self.ShipmentCancelRequest).from_(gateway).parse() + ) + + self.assertEqual( + to_dict(parsed_response), to_dict(PARSED_CANCEL_SHIPMENT_RESPONSE) + ) + if __name__ == "__main__": unittest.main() +SHIPMENT_CANCEL_REQUEST_PAYLOAD = {"shipment_identifier": "329014521622"} + SHIPMENT_REQUEST_PAYLOAD = { "shipper": { "person_name": "Aaron Summer", @@ -118,6 +153,7 @@ def test_parse_invalid_shipment_response(self): "carrier_id": "purolator_courier", "label": "JVBERi0xLjQKJfbk/N8KMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovVmVyc2lvbiAvMS40Ci9QYWdlcyAyIDAgUgo+PgplbmRvYmoKMyAwIG9iago8PAovUHJvZHVjZXIgKGlUZXh0IDIuMS43IGJ5IDFUM1hUKQovTW9kRGF0ZSAoRDoyMDE4MDIyNzA4MDg0OSswMScwMCcpCi9DcmVhdGlvbkRhdGUgKEQ6MjAxODAyMjcwODA4NDkrMDEnMDAnKQo+PgplbmRvYmoKMiAwIG9iago8PAovVHlwZSAvUGFnZXMKL0tpZHMgWzQgMCBSIDUgMCBSXQovQ291bnQgMgo+PgplbmRvYmoKNCAwIG9iago8PAovUGFyZW50IDIgMCBSCi9Db250ZW50cyA2IDAgUgovVHlwZSAvUGFnZQovUmVzb3VyY2VzIDcgMCBSCi9NZWRpYUJveCBbMC4wIDAuMCA4NDEuODkgNTk1LjI4XQovQ3JvcEJveCBbMC4wIDAuMCA4NDEuODkgNTk1LjI4XQovUm90YXRlIDAKPj4KZW5kb2JqCjUgMCBvYmoKPDwKL1BhcmVudCAyIDAgUgovQ29udGVudHMgOCAwIFIKL1R5cGUgL1BhZ2UKL1Jlc291cmNlcyA5IDAgUgovTWVkaWFCb3ggWzAuMCAwLjAgODQxLjg5IDU5NS4yOF0KL0Nyb3BCb3ggWzAuMCAwLjAgODQxLjg5IDU5NS4yOF0KL1JvdGF0ZSAwCj4+CmVuZG9iago2IDAgb2JqCjw8Ci9MZW5ndGggNTEKL0ZpbHRlciAvRmxhdGVEZWNvZGUKPj4Kc3RyZWFtDQp4nCvkcgrhMlAwtTTVM7JQCEnhcg3hCuQqVDBUMABCCJmcq6AfkWao4JKvEMgFAP2hClYNCmVuZHN0cmVhbQplbmRvYmoKNyAwIG9iago8PAovWE9iamVjdCA8PAovWGYxIDEwIDAgUgo+PgovUHJvY1NldCBbL1BERiAvVGV4dCAvSW1hZ2VCIC9JbWFnZUMgL0ltYWdlSV0KPj4KZW5kb2JqCjggMCBvYmoKPDwKL0xlbmd0aCA1MQovRmlsdGVyIC9GbGF0ZURlY29kZQo+PgpzdHJlYW0NCnicK+RyCuEyUDC1NNUzslAISeFyDeEK5CpUMFQwAEIImZyroB+RZqjgkq8Q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74WUwoWAXyykecYrIv7HHFQlq+3XXSON0ttX+3a12ft0+DUQ/lcEdnj18OVLyduuF5t2vZmvbkW3Wux/brP7cPjNvjznKO9VfhP80oizRTtdt+K6E6vuBaODPJTi8EXSxvw+vxHru/n904cP5ROOavvwUfQ/PGTml13lzfa7J+Sd8t3z/XzWrvCr2YI+03Y34p5ewsR8Rf3ja92zH4/x0laWxcSQ8oPO9ycSrvXGOxPhgqKTN+E5D9xY5EByhmRlyg6wMbjsuANxP71twQiGq8QBp/0If/4PWx5djw0KZW5kc3RyZWFtCmVuZG9iagoxMiAwIG9iago8PAovTGVuZ3RoIDcxNTUKL1R5cGUgL1hPYmplY3QKL1N1YnR5cGUgL0ltYWdlCi9Db2xvclNwYWNlIFsvSW5kZXhlZCAvRGV2aWNlUkdCIDI1NSA8RDJFNTlDQkNENzZCRTlGMkNFRkFGQ0YzQUJDRDQ1QzdERTg0RERFQkI1RjRGOUU3QjZENDVFQjBEMTUyRUVGNURCRDhFOEE5Q0RFMTkwRTNFRkMyQzFEQjc3QkJENzZBQUFDRDQ1RThGMUNERjlGQkYyQzZERDgzQjVEMzVFRjNGOEU2RUVGNUQ5RDJFNDlDRERFQkI0QzFEQTc2RDdFN0E4QjBEMDUxQ0NFMThGRTJFRUMxQTVDQTM5RkZGRkZGMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw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q3/orhbLw28n393sXfKHKtar1X7xYr9ajbjIbrW6fX0Ri9VK+gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQ2v8B4fNgUg0KZW5kc3RyZWFtCmVuZG9iagoxMyAwIG9iago8PAovRjEgMTYgMCBSCi9GMiAxNyAwIFIKPj4KZW5kb2JqCjE0IDAgb2JqCjw8Ci9MZW5ndGggNzE1NQovVHlwZSAvWE9iamVjdAovU3VidHlwZSAvSW1hZ2UKL0NvbG9yU3BhY2UgWy9JbmRleGVkIC9EZXZpY2VSR0IgMjU1IDxEMkU1OUNCQ0Q3NkJFOUYyQ0VGQUZDRjNBQkNENDVDN0RFODREREVCQjVGNEY5RTdCNkQ0NUVCMEQxNTJFRUY1REJEOEU4QTlDREUxOTBFM0VGQzJDMURCNzdCQkQ3NkFBQUNENDVFOEYxQ0RGOUZCRjJDNkREODNCNUQzNUVGM0Y4RTZFRUY1RDlEMkU0OUNEREVCQjRDMURBNzZEN0U3QThCMEQwNTFDQ0UxOEZFMkVFQzFBNUNBMzlGRkZGRkYwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA+XQov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", "tracking_number": "329014521622", + "shipment_identifier": "329014521622", }, [], ] @@ -139,6 +175,17 @@ def test_parse_invalid_shipment_response(self): ], ] +PARSED_CANCEL_SHIPMENT_RESPONSE = [ + { + "carrier_id": "purolator_courier", + "carrier_name": "purolator_courier", + "operation": "Cancel Shipment", + "success": True, + }, + [], +] + + SHIPMENT_REQUEST_XML = f""" @@ -353,3 +400,44 @@ def test_parse_invalid_shipment_response(self): """ + +SHIPMENT_CANCEL_REQUEST_XML = """ + + + 1.0 + en + + + token + + + + + + 329014521622 + + + + +""" + +SHIPMENT_CANCEL_RESPONSE_XML = """ + + + + Rating Example + + + + + + + + + true + + + +""" diff --git a/tests/ups_package/address.py b/tests/ups_package/address.py index 0f9df3c04c..719479fce5 100644 --- a/tests/ups_package/address.py +++ b/tests/ups_package/address.py @@ -60,7 +60,11 @@ def test_parse_address_validation_response(self): } ParsedAddressValidationResponse = [ - {"carrier_id": "ups_package", "carrier_name": "ups_package", "success": True}, + { + "carrier_id": "ups_package", + "carrier_name": "ups_package", + "success": True, + }, [], ] diff --git a/tests/ups_package/pickup.py b/tests/ups_package/pickup.py index 161ef0aaa3..bf815ab3fc 100644 --- a/tests/ups_package/pickup.py +++ b/tests/ups_package/pickup.py @@ -6,19 +6,19 @@ from purplship.core.models import ( PickupRequest, PickupUpdateRequest, - PickupCancellationRequest, + PickupCancelRequest, ) from tests.ups_package.fixture import gateway logger = logging.getLogger(__name__) -class TestPurolatorPickup(unittest.TestCase): +class TestUPSPickup(unittest.TestCase): def setUp(self): self.maxDiff = None self.PickupRequest = PickupRequest(**pickup_data) self.PickupUpdateRequest = PickupUpdateRequest(**pickup_update_data) - self.PickupCancelRequest = PickupCancellationRequest(**pickup_cancel_data) + self.PickupCancelRequest = PickupCancelRequest(**pickup_cancel_data) def test_create_pickup_request(self): request = gateway.mapper.create_pickup_request(self.PickupRequest) @@ -30,7 +30,7 @@ def test_create_pickup_request(self): self.assertEqual(create_request.data.serialize(), PickupRequestXML) def test_update_pickup_request(self): - request = gateway.mapper.create_modify_pickup_request(self.PickupUpdateRequest) + request = gateway.mapper.create_pickup_update_request(self.PickupUpdateRequest) pipeline = request.serialize() rate_request = pipeline["rate"]() create_request = pipeline["create"](PickupRateResponseXML) @@ -43,7 +43,7 @@ def test_update_pickup_request(self): def test_create_pickup(self): with patch("purplship.mappers.ups_package.proxy.http") as mocks: mocks.side_effect = [PickupRateResponseXML, PickupResponseXML] - purplship.Pickup.book(self.PickupRequest).with_(gateway) + purplship.Pickup.schedule(self.PickupRequest).with_(gateway) rate_call, create_call = mocks.call_args_list self.assertEqual( @@ -82,7 +82,7 @@ def test_parse_pickup_reply(self): with patch("purplship.mappers.ups_package.proxy.http") as mocks: mocks.side_effect = [PickupRateResponseXML, PickupResponseXML] parsed_response = ( - purplship.Pickup.book(self.PickupRequest).with_(gateway).parse() + purplship.Pickup.schedule(self.PickupRequest).with_(gateway).parse() ) self.assertListEqual(to_dict(parsed_response), ParsedPickupResponse) @@ -101,7 +101,7 @@ def test_parse_pickup_cancel_reply(self): unittest.main() pickup_data = { - "date": "2015-01-28", + "pickup_date": "2015-01-28", "address": { "company_name": "Jim Duggan", "address_line1": "2271 Herring Cove", @@ -121,7 +121,7 @@ def test_parse_pickup_cancel_reply(self): pickup_update_data = { "confirmation_number": "0074698052", - "date": "2015-01-28", + "pickup_date": "2015-01-28", "address": { "person_name": "Jane Doe", "email": "john.doe@canadapost.ca", @@ -149,6 +149,7 @@ def test_parse_pickup_cancel_reply(self): { "carrier_id": "ups_package", "carrier_name": "ups_package", + "operation": "Cancel Pickup", "success": True, }, [], diff --git a/tests/ups_package/shipment.py b/tests/ups_package/shipment.py index dae248d1d4..1e4ff71766 100644 --- a/tests/ups_package/shipment.py +++ b/tests/ups_package/shipment.py @@ -1,7 +1,8 @@ import unittest +import logging from unittest.mock import patch, ANY from purplship.core.utils.helpers import to_dict -from purplship.core.models import ShipmentRequest +from purplship.core.models import ShipmentRequest, ShipmentCancelRequest from purplship import Shipment from tests.ups_package.fixture import gateway @@ -10,11 +11,22 @@ class TestUPSShipment(unittest.TestCase): def setUp(self): self.maxDiff = None self.ShipmentRequest = ShipmentRequest(**package_shipment_data) + self.ShipmentCancelRequest = ShipmentCancelRequest( + **shipment_cancel_request_data + ) def test_create_package_shipment_request(self): request = gateway.mapper.create_shipment_request(self.ShipmentRequest) + self.assertEqual(request.serialize(), ShipmentRequestXML) + def test_create_cancel_shipment_request(self): + request = gateway.mapper.create_cancel_shipment_request( + self.ShipmentCancelRequest + ) + + self.assertEqual(request.serialize(), ShipmentCancelRequestXML) + def test_create_package_shipment_with_package_preset_request(self): request = gateway.mapper.create_shipment_request( ShipmentRequest(**package_shipment_with_package_preset_data) @@ -46,11 +58,21 @@ def test_parse_publish_rate_shipment_response(self): ) self.assertListEqual(to_dict(parsed_response), ParsedShipmentResponse) + def test_parse_cancel_shipment_response(self): + with patch("purplship.mappers.ups_package.proxy.http") as mock: + mock.return_value = ShipmentCancelResponseXML + parsed_response = ( + Shipment.cancel(self.ShipmentCancelRequest).from_(gateway).parse() + ) + self.assertListEqual(to_dict(parsed_response), ParsedShipmentCancelResponse) + if __name__ == "__main__": unittest.main() +shipment_cancel_request_data = {"shipment_identifier": "1ZWA82900191640782"} + package_shipment_data = { "shipper": { "company_name": "Shipper Name", @@ -91,7 +113,6 @@ def test_parse_publish_rate_shipment_response(self): "reference": "Your Customer Context", } - package_shipment_with_package_preset_data = { "shipper": { "company_name": "Shipper Name", @@ -137,6 +158,7 @@ def test_parse_publish_rate_shipment_response(self): "carrier_id": "ups_package", "label": ANY, "tracking_number": "1ZWA82900191640782", + "shipment_identifier": "1ZWA82900191640782", }, [], ] @@ -147,10 +169,22 @@ def test_parse_publish_rate_shipment_response(self): "carrier_id": "ups_package", "label": ANY, "tracking_number": "1ZWA82900191640782", + "shipment_identifier": "1ZWA82900191640782", }, [], ] +ParsedShipmentCancelResponse = [ + { + "carrier_id": "ups_package", + "carrier_name": "ups_package", + "operation": "Cancel Shipment", + "success": True, + }, + [], +] + + NegotiatedShipmentResponseXML = f""" @@ -476,3 +510,50 @@ def test_parse_publish_rate_shipment_response(self): """ + +ShipmentCancelRequestXML = """ + + + + username + password + + + FG09H9G8H09GH8G0 + + + + + + + + 1ZWA82900191640782 + + + + +""" + +ShipmentCancelResponseXML = """ + + + + + + 1 + Success + + + Your Customer Context + + + + + 1 + Voided + + + + + +"""