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\nY/2gFzvayNgYn7tBUiIuDCFhU/CsgjFY3Rj6TQ8TN9I1bqtmO8uygMXXMTQ4CosjhHVKGh1DZPJX\nik1UXCc0zxKFTXklIimVf9Oa+spNYcjgNVTa0HQSqoDzQGUj2YYmkNSWfjT4Y8D22NDaQeE6+Saa\nQS9I4+SNx+U00jQxDPPBIdEQSM3qyeLsjfKNVQNsCdo0PowWCLeOa5hjGjvH7Ws0OCoMjlqSDXU6\nOFJZ2iHDmhudHb/VCnZH3m2W7GH/G8W1w85He4Nu3so8BJ/30jANOGVKsWHDNHFDjzo13tTT0CHk\nqfnGWDoxIa8z6QVpjX04PA2L0b7oilHGgacuqbEf1Cb3KzVnFF0xLDoSGZ4iRtYL0oq5iNnLOKyH\nc3k9aGywHrhYfhwr6mjHuW3ti64Y5WQYC2sa2RqCDn4cF7MnTQzrG4iTo18Gm2Hvjprmj/bG1uth\n4FRjTBkimTE7cULDdtNMwwC8290fqfJLrQ1kBNr0gaIUKDoSsOwWzxtsb4ycuHbZ1mx3ljGUs7bn\nC8caPV8j13g2XV2fnhtIQU5biylSUorEP+ffTZInbE4S47INSeNiyBSg6xSHgRv2l6IJUYpTxsac\nKt723VK84dniLwHjgzOGjXHISeigcJY2JuTMluL1fHkrxUknfhzTKsQm3iccTzxr0CeEPepTewuh\nDvt8d/SDOO76+66fbubdam+W/gpOQj7JSdoBjmg/9OK+3bT9CzAQDk1ZCa0hOBJHiQ/to/jYz+6m\n/bU476bXzydaqG4g9BPRqRRy+XDYL7vV5xdQcjrO40rOlnFJJy2N7Zeu/7z9y0uwCjdemW4MGftp\nNd9APXOxmW7atfh4Iw6XbT+fTfdyZcXVAAwBuBrTaCnBNKR95B53KyiVNm/ALkF5Ej3sNamfUYnh\nkZKq2qZWh5id/LGf385Xb55gcJI4xz0cSPIm10VQmRlr8n7/cPVfDslnyoaUw3s5U0X/45mKWPdY\nH9E+nykXUh7sZVefKIXF3iPUuXSuoFbFLL/cagjVBmOjUrSJtvqCIh/UENsWgx4sDIQjCB2sxcDM\nGXzsA/4vadbHoMc+hhaDHiyGPsYWxX8Rq889uy3gcpfdZi0s0nDkj/qHdSvO+vnfsOXE+/mSNuBx\nt7yfrr6ID9Nlu2eXUH20pxcvcV5l78VoSiiAndwvpqvrveHgaywncXnziF2M+fAmcfSwnq/a9Vqc\nTeHcHberTT9dCPUCMBQrZSVsgIhLXLO5E28XXfeSAUJCCeWgSZtSXtLLu1bgsp4tprOnK4dbFC82\ne2iKKtIcqyycjBJlvCQPrdvF+iU0maNOdkTSOQgctYvb+cPyObEkQTvYNAqrxeyCGFWoQsnzMd7i\nJiWMT3AtJIyT/iA6daAhj1XXom2aLVEA0wILAiRZDIDAJCv3YCkP96Io6d6VA02CapDG/tN3e1ZR\n06WJr6Kle6fJAQcYCg8bLeHpwdH5p4PDDyf7vLE7FIOcyMbyqsFm2wgHa4H10XZyRbPjvLuiZXLe\nYc4fEng5Z2d9d/2wzzFKBGz1FOMC3v7zDtaupIpfz34Xpz+fnZ9eXIirj+fvT67enZyK317Z+Ns3\nz/D5gIbQWyJw8imTz6ZflnBexay7bp+zfQrKGnIP7WWtysl4e34pDl8fvxHRBqiakkvPB0LsTKms\nngxl2pdtv1yL7kZc9lMYnjg5PHsBUeK9LxNVSeUnl2c0wuecE6jtoBV3q4Lomffs23a6eeihGngt\nLtr+7/msXe9jWipS9jBl8fEBRv7yInN2eHZ6/h49fHl+SN69+qX2btmgmu6D4wbNutqgRtUpGLoR\nRoUxAyvr80zO25s3Av6n7dvVrH339Ah9FQe1shoKVjhNOWYfP6w3UCb/NF28ITspxaeLfVC3EyQ1\nXPsh7vNBwr7ymXpxd78RV7d3e8+RovcCjgoGb2t4Fx9QIUSXKw4NYxKfb58xTQ3RH28sfJ5eal9G\nNL+nc3MCGXrfqPy+ETFU8nI7IhUPpDmQ/hmDMhDPkqt9GZzNWeBfeFTO5i3sxn3rZKgG5ZEwP07l\nc/dKlTD4j17X0eENrRTeUdohwUK9MuuWy+56vpm363+LTTu7W3WL7vaL6Hqx7m42j9O+FY+wz0T7\nP7jQYEVzg1eqDWTnqsR+9hbUYUinypQDPl+J6WzW9ddT2M3icQ4lBPJPqUdxeL2cr+brTb5Nib69\nfVjQX9eNOJn/3fZr/OdZh/VL/+UF45DbF1ZYoJC3yKYTn5oLsZg+ivu+u5v/gVNsnp5nDTVrcuN5\nLppqVPAZdKgdvWhCylMWrnODXgxaRYsF/GKw3+o7+Bd6QWCEgHBGIM0IZF8RYMdFRtAKK/yRkDUj\nkD0nYC5PnODwBYYRSI+EbF8RfMMnAfcWKP8YgDQDoHnVPuL5Y4CEbyIMQJoByJ4TDCY6RoBCs/JE\n1iMh21cE0yTDCXBT4gCUrD1ZV+0DWo7tLYZxBsiaEcieE6DIVpYTdO2HrEdCtq8IpvKDtbUfsmYA\ns+OH/I7GAHCgeHuUrLnb9QLUbjGw9o7eOEdA1oxA9pwAppbvZjhzku/mrEdCtq8Inl56RkLAoM4I\npBmB7CtCxGvFSIByIHBC1oxA9pzgFb5pMoLGLwOMQHokZPuKQHd+Rgj0Ij8SSDMC2VeEiCl8JATJ\nmoNgbcmStw30GYG11bUXsh4J2b4imNoLUKVXXsiaEcyuF4KrvRA8vs8yAmlGcLteCJG+TIyEVHsh\na0Yge06AS0jkByqahjuB5Ng+W1ft4W7C1zHSgw4DkGYEsq8IcD/iMSEmfK1hBNKMQPackPBWMQKS\nwlw4ArIeAWRetTcN30bJYonM2pNm7dG8au/wiY0BIl4QGYA0A5B9RUj4KLQlQEFW5ciiGYHsGUFj\nFRU5wVU5suhti2JfEXyVI+EO1HjNCaQZwe/kSI0f8TgBvwdzQtYjIdtXBF1FZ6i/qyxZNCPonfis\nsR7RnEDfjRiBNCPYnSypsR6pCLH2RdaMEHbqFSzbE18HqE8kX4esGYHsOUGb2hfaV3my6JGQ7StC\n4HlS61jlyaIZINR5UmM9wj0B1UVke7poBki7njC6ypQa6wkOQDm2z9ZVe1tVK9o4+u46Akgzgt2p\nV+BOiF9bRoJVVb1SNCOQPSdgPVIRTO2HrEdCtq8ItvID1BchcgBpBrA7frD5E/IIiLUfsmYAsq8I\nqfaDk5UfSLL2adcPzlT1isZqgvsh65GQ7StCqDKldqnxFYE0I4SdTKmxHmEALCb4bs56BJB51V5X\neVJjLVEBSDOA3smTeCUyFSFW1UrRjOB3qhX6MMe3QqD3xZGQ9UjI9hUBTjn3BFQYnu/nrBmB7CuC\nw4caRvD4/YYRSDMC2VeEUHkCCwq+kFkzQNjxRFTVfVJHXeXKokdAtq8Ips6V0dXxOWtGMLu5Mvo6\nV0KFUXkia0bwu7kSSojKE1BiVJ7IeiRke0a4Ge7zu2+wCnsO9PW8PPj48rZ5dfjL0bv374WUwoWA\nXyykecYrIv7HHFQlq+3XXSON0ttX+3a12ft0+DUQ/lcEdnj18OVLyduuF5t2vZmvbkW3Wux/brP7\ncPjNvjznKO9VfhP80oizRTtdt+K6E6vuBaODPJTi8EXSxvw+vxHru/n904cP5ROOavvwUfQ/PGTm\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
+
+ 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+RZqjgkq8QyAUA/aEKVg0KZW5kc3RyZWFtCmVuZG9iago5IDAgb2JqCjw8Ci9YT2JqZWN0IDw8Ci9YZjEgMTEgMCBSCj4+Ci9Qcm9jU2V0IFsvUERGIC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJXQo+PgplbmRvYmoKMTAgMCBvYmoKPDwKL0xlbmd0aCA0NDgxCi9UeXBlIC9YT2JqZWN0Ci9SZXNvdXJjZXMgPDwKL1hPYmplY3QgPDwKL2ltZzAgMTIgMCBSCj4+Ci9Qcm9jU2V0IFsvUERGIC9UZXh0IC9JbWFnZUIgL0ltYWdlQyAvSW1hZ2VJXQovRm9udCAxMyAwIFIKPj4KL1N1YnR5cGUgL0Zvcm0KL0JCb3ggWzAgMCA4NDEuODkgNTk1LjI4XQovTWF0cml4IFsxIDAgMCAxIDAgMF0KL0Zvcm1UeXBlIDEKL0ZpbHRlciAvRmxhdGVEZWNvZGUKPj4Kc3RyZWFtDQp4nJWbW3fcxpWF3/kr8GhnRhDqXtCbbs6SR7YZiY6cNZ4HDtUSO0az5WY7Gv37OZdC1a5WK8uK1kq4w7M/oC44daoA/n4xDWEOo83Djn6chuUiezPmGX8sAcvF7cWbi7sLM3y8sMP3FP7PCzMNP1z89/9Mw9uL38U/DYf3F0+uLh5+ZwZjRuOHq3fk4F+YYc6Cc2lMYbjaXTyYRmvjnIarm4tvnv9y+er569fDm59evXz25sWz599e/ZOQ9KvnV0q0gx+DQaCx0+j9EGwep5mJ0+hCMFaAdjL5wWQf2DT88sPLyxdDHO3wcPjLPD0wcfJ/Qb7c+jhNbrbcAGs932IIdkyResYmunJa9VK1jWPwrEv8qm8v+B75X+sNa8fg8OatDeNEJjcxlLvDjFOUW39z+ctndyf3NVOIG4KnATF8XzlzjwQapzTTdd1k+RednlfJIquiG2EVgqgpDTfNOo3R0G/NGKlj3TxmEXYW4ZyE0sAa0RP/dmJ6cHmMQWJzWBXHptHHVS8n2k15dFEvw6RZuHQLM1vNxPMneDs6+Z1dG+6mMIaqbk66ZaHOtzmNfBee75D6abY8MEKW8eKZaFDPHK6ShZ+1J7Ioo7+zVq6lVrplGjVW1klzZlaJ28HKZ4mlmS7SevmlyzL7yc9GnrmquJ/imN2qF9HceBe5O93kxpzW9vAtyS2Iuukby613Jo7erLAdaTfG1CIY7gNqauPcLs7T11kaqLQO1E5i5KLrQBovM2Ttc2d14Ks2Mz+TPL+8xNP8dwZ14Iu3eMfDXKXeQtS5tDaDnjJj4LapmZJPqp7H6Ppm2JS0aXF0Xp6YpP0sehFt+RqBn4alxRfdMcp98FM3m3Yd1k6vO1lkFN0xPA8kM6JkDNWLaANDBPFTXvsjBO0PuTfqD+6s2O6VdfatbTW+6I5RngznqU8z9CHpFNt9QbxoYfg4Up5s47LGrHO3aWk/xzvf94ejp5pzyprJnDvJE5ammwVNNxDD6fyYu3HptaNFQSZ9kixFSh4J6nbPzxtNb86c3Hca6+rMco7yXR2LG7nXHLGPwhihubZ/et7RKhSs97xOTrJO8r9Xf72Yo2B1kWjdti4ar9eVInIyw1WOUzVd0EiDZJUz1k+6bH532O+GR7ha/D7Q/dEzxma+5XmwyXArfZ557t/shofb3ftpeLYf/raurJFzU7eyBnnW6JqU9uSaNnpKdXzNF09+GJ7uDx/2h+vjdn93ZqH+Im6i9aSs04QT2g+H4cPmuDl8BYbSoSs9Ya2b9K7M8OPm4/DT4eb2+vB2eLW/fvvniZ5KHEr9QgxmTlmIjw+7/d1vX0HR5Vjvaw6+3NcUJi/39o/94bf6w9dgDU+80tycFPvz3fa4eTu8Pl4fN/fDT++Gx7vNYXtzfZY7dVxLwJSIa3kZ1eFIlpZ95j7d3x2vb46PuF6j8iRH4/xkz1K5bGlUfqQm001Tb1PWQf7psH2/vXv0GYYbyW08w6FF3sVSJk7OOy2NfnzzXwjRZ8qnWdN7eaaK/rfPlDytPmaO12fKB8pIfI2rff9EGS72PlKxK8+Vj5FX+V3VlKod50ZjZBJV/VoyH9UQ1bHqNcJROqLUAY6VqSt4uwb9z2zhGqtu11gdq14j1ms0Rxm/NJ6dbGmSrpLJ5qKtD/yTwx/3m+HysP0XTbjh5XYn0+/pfvfh+u7T8OP1bnNmjkh1dOYiceJWlZmXsyuJgObxYbm+e3s2GXyJFSbuXL3hkLM+uvPw5I/77d3m/n64vKan7unm7ni4XgbzFWAqVUpH+OS8ct3xdvhu2e+/5gZpOUnlMZv8XLY7V7ebgbv1crm++bzneIKOHPk5zUg9qpnK03NRckycZITuN8v919AmzTk6ELPVFPBks7zf/rH7U5kk6YQ2XCzqGORsUpdJvoITPU9S4cTZJW1cmOKDHMwDS+tYty2qy2zJArwsQBIQCROeKlDORqf7UZen0qO0ATM6FZ/+/a9nepFK3G4/a4Lhwo8BTmZK9T95/uDJq58fPP7x2ZnGU73AiQYynne8fXCUBrT134xsq+mNOoKLo9qyorlpJwGGtl+U0mvAqoPjHcmyhq+Sq6iTrW9P4EI18K60Ele9Ik6vcG7rSyvCafYtpshQ6Tinmebpg8uXV2f3vqXzApf43WohRb+LMxeGa2mRdbG42p7NSp8xJs9PKDIMFeRBGM+uP32+4JTmthFZm18XnHyyqhnaJlsZ6bQmK9qG6fP2avPu0UD/tTls7m42L87NmdNbtokf/0GSfTn5sNM0aSde3mwevr79cBzebLbvb49/pgd4TiKO11wfbeFtN2dzVOYqAm+K1jnqG+drFZSnVLrRjtNDS9Dht/fn8pOTSgRvyPEK4ZwbpSxz45rpHp5L4ZHL9897m0sRv9ZjefJr5qWF7Ga/2+3fbo/bzf1/DsfNze3dftm//zTsD8P9/t3x4/VhM3ykARk2/0d1Li9177jSPlLa7iqvr7iZSbpLZqhxpYrb3g3XNzf7w9trGvnh45aWF77Ec7no8Pjtbnu3vT9qnT0cNu//WOTH+3F4tv3X5nDP//fNnte2w6c/fyt8LLTWqTkkfViO++Hn8fWwXH8cPhz2t9v/5VaOn899Pkxxc5v7RWv1MvNywjkicyY2hi+w6qXq4EvFVOJXfUsRclIDBMfnJEBQXQklHgme5woQaE4jQGTzazT6+TQF7HrQ0/yqG0DCwU+zPEYA0KoREFB0BZR4JFhOi0CQ7Q4QVDeCxiPBy0g1QuDTBSCobgSNRwLtxx0SZjkNawTVjaDxQHCUoXAuOMunE41QdCWUeCQ43qwDQY6cgKC6ETQeCXI0AgQ5+AKC6kbQeCRQad4RMp8XAUF1I2g8EDxvKYHgbT+hiq6EEo8EOb4DgpTyQFDdCBqPhMilDxDSmBwSVDeCxiMh85LRCFp1N0LRjaDxQAhyCg0Ey2sNEFRXQolHgpfz2UZII3aDyubXaPTTooz27urdpSUQnTMnzGal/Us24C66ATQeCJFa0xEsn7MDQXUllHgkyBoJBKrIsPlFN4LGIyH28ygmeEKXqhshns6jKCfbjUBVSDePim4EjQdCMvAMM8FxDQ4E1ZVQ4pHg+3mUwoipTWXz+9NZlGA9YDuk+2WVzd6tFuzG5YDsGbP9UnUD9MsFETKuB0xw8GwuVVdC7tcLJki5CoTIuzsgqG4EjUdCgqeXCPPEO5dGKLoRUvd0E0FfuQDBwLO5VF0JJR4Jns8lgBDkrLIRVDeCxiMBu5GKk84tspn7HuQXNc1LlTPvaap51c0t4c3PAXhxy2+9OoDq1bHGI8F288jSZjV2BNfNozUeCbgKMCHzST4QVDdCv0owQV7gAYGyVkdQ3QgaDwQzdfPIUnVmYBasuhLMdDKPLNVfk0eC7+bRqhtB45EQoK5ggrxZAYLqRghd3cEEmIaWyjOLA1F0s3dz0FLtZXAcqTbrplLR1V7ikeD6uWQjH7kDQXUjuNO5RLWXwZnAr8M7gupG0Hgk5HF2SJA3BUBQ3QgaDwSq5rq55LAKX6quhBKPhCCvSRuhS8xFNr9Go5/WGrTT1gLtIptdgsHN73fB7bEEX6qufg1Hv+PTagDQFhvHoOgG0HgkJH7FBoTMRwVAUN0IGo8ErNGJEKZufV91I/Q1PBGo5vIdwcJ+bqm6Eko8EhwfbgEBs/xSdSNoPBIi7PiYgBX4UnUjxG5HyASs0YlA1RkCVDZ/X8GTn2ovb9B/kpvjSW4u8Ujw3RpvY+DzUSCobgR/ssa/g1P6uT9zdFIm0+jXY/oYy+HIm8f/ePLi5cthmoaQEh9hT+7s6U//GY2l5Z6fFVsP5alTymkJH+Ju7o73j4bv9ofhuLk/bu8+P8H5IjOF+n7FTnxLzNzfLcOncbhcNtf3m+HtfrjbD+dPqc4yo5xV6UHKXF6y3d9uP3SvbGjlceSgx0zf1RgpC1UuRUqtaIZlja76Vo4JuA5tfvnECACigSDxSOBvkxIQaCGJeAuqG0HjO4LO7EbIkoUbQTQQJB4J/CYbe8FpNVwJqhtB4ztCkiqiEbKcPTSCaCBIfEeYZW5XgpfvnRpBNRAkHgle145GiFLPNoLoRtD4jpBkP1YJnLewH1QDQeKRUHanjSA/AEF+aASN7wjyhQMQ5NMGIIgGgsR3hFnO2yqB9owW54NqIEg8EijvZOyHbkJ2s1Ej0Zt09ajeZGWXUe2qG0HjO4KX2qcR5A0vEEQDQeI7QpZqsBKy7lMqQTUQJB4J/M4be4D2St1zrboRNL4j6J62EigpZewH1UCQeCTMVmrCRnCyAjWC6EbQ+I4QZFfcCLPUiI0gGggSDwTekXjoB95fwHRWWeNLdOcPXV6g5M4vyQEgGgjhJC/I7gJ6gfcKwQNBNRAkHgm0F0gOCbrDaQTRjaDxHSF3qwQV92NAAEvw55NVgjcHuErwJ50ee0F1I2h8R4hSSzTCzB+FAkE0ECQeCZSzPfaC0/ODSlDdCBrfEdLoEZDHMCNANAA4HP383STOBSqWDfaC6gbQ+I7guzWCq+WEvaAaCP5kjbD8TRo2gmtd7AXVQIgnmYFr2YRzgetK7AbVjaDxHUFeYjdC1K1jJagGgsQjIUoJBgT5bgUIohtB4ztCgkzAhFnOFRtBNBBSlymIkKYRG5GcFOwVoLoBJLzze9kjNEDk914AEA0Aie8Ic7dO8OdKXWZQDYT5ZJ2w2cn+tBEiPOfLqhtB4ztC6tYJy9/OYTeoBkI6WSf4tZjFfph9V/8V3Qga3xES5IKdfKXssR9UAyF1ueJWvtTOMBv4RbeFfii6Okp8RwiYG/jzbSwgiwZA6HODfMQNveCM6erHohtA4zuCg3pxJx8ezwEJooHgunqSCamrH/lTZIO9oBoI6aR+5LfRDgEBisFl1Q0g4Z1/lvdrFcDvvrANqgEg8Uhweu7VCKGr/YpuBI3vCFHeDzRChlpwWTUQJB4IX96T0gPJE5GyU1i/mKKCWF/8//qNffnrt0+e8ydK/+HzxP8xZ7Zqxhs+IdGtGneG6mXVyemH4yV8lfrC1lj0Z0kozS+6+SUc/LTxmtHP0wH9qqtfw9Evn5CDP8tS0/yim1/C0T/zwU7z85fw6Ffd/BIOflrgQ+dPfftVV7+Gg58COaNXv9dFpfpVV7+Go9/J9qv5I/THsurml3D0J+gv3Zx17Vfd/Am7k7dFoR9/WqjdjH7R1a/h6E+y8ar+OEF/LKtufgkHf6TFA/svymf54Bdd/RqO/lnK++qnFbVrv+rml3Dw89fdeH3eRuH9q65+DUe/lpTVnw2M57Lq5pdw8NMWqWt/Dv38V139Go7+3LefNlAB71918+eT9s/ysgX8vm+/6urXcPRHWfybf5aNd/OLbn4Jb37eHCXw86sbHP+iV0MJR7/ntAn+BP2xrLr5JRz8/FIF2m/5i2oHftXVr+Hod9BfZRvV+QM2uISjf+7bbw2YrQHnfNJy68eMxsTfUYJXdLVLNLrziM2mNOnBLLJ5ORa8znVz3uqfn4A74ZiXcPTnvs1+AjOJ5swnbfa2H21ef9HroJElGN2B/1yvucPUj7Xq5pdw8POxVOd3/Virrn4NR3+UDW3zz2iewSmB4Iy+v3PKit1gq65+DUd/7EabkiKOtshmjv1o89GUR3OAViyrrnYNRz/O0qS1WTOLbuZujmb5TLOZKZfiaIusVg1Gt+v7LOvBZrOLbn530me0zcmdf+46TWRzSzC4KW92Yz0H/ryq2VVXv4ajf+7azpuNCKmh6Oaf+9bzXgLvnvcaxqNf9Goo4eiXP/kC/wxzf1l180s4+M3UzRreaGD7i65+DUd/5L9jaX4rf8bT/KqbX8LBz38xidfn/Nf5RVe/hqM/y/FC9XNGRL/q5pdw8OvndOAP0J5l1dWv4eiPsqFtft1gNL/o5pfw5v/ydiPK9y2UMEL9Y7Kp7ja+//Xb4ftnE3/VLX9azJewdh6iSzNuO/5G//4fvwaz6g0KZW5kc3RyZWFtCmVuZG9iagoxMSAwIG9iago8PAovTGVuZ3RoIDMxOTgKL1R5cGUgL1hPYmplY3QKL1Jlc291cmNlcyA8PAovWE9iamVjdCA8PAovaW1nMCAxNCAwIFIKPj4KL1Byb2NTZXQgWy9QREYgL1RleHQgL0ltYWdlQiAvSW1hZ2VDIC9JbWFnZUldCi9Gb250IDE1IDAgUgo+PgovU3VidHlwZSAvRm9ybQovQkJveCBbMCAwIDg0MS44OSA1OTUuMjhdCi9NYXRyaXggWzEgMCAwIDEgMCAwXQovRm9ybVR5cGUgMQovRmlsdGVyIC9GbGF0ZURlY29kZQo+PgpzdHJlYW0NCniclZrbchs3Eobv+RSo2hsntRrjfPCdTs4669iKJEdJJblgqJHEmOQoQypav/12NzCcBkWnpHLVrn+z8eHQQHcDk78mUrjkGh3FEv4qxWISrWpi4n8tBovJ3eRqspoo8TjR4nsw/3OipPhh8uvvUlxP/qL2UvS3k6PLyeu3SijV6CAub6AF/oD/IJtkhTOpUUZcQo+NCdZ7cTmbvPpWHJ4f/+fdT6fi5OOx+Pabyz8BCD+cXmaeFr4J/gkuCKdjE1TBqSAj4T50G7HpxB+tmG4209lde43yfjr7PL1tOZxG3Uhpksaxa22b4IRzGntbTnSwjQuDXmy19o2zqIv9oO8mOED8My6E1o0zfORau0ZCIyNpSsvJgWpkXoars5+fjI7GlcDECGfBFwrHFWPjFGjVhAT9Gqnxh0qnQaKIWcFAUDlHSgYxG5vKxiv4VTU+OymS0ImEMWSqGqVIS/xVIt2Z2HhHttENCm1DY/2gFzvayNgYn7tBUiIuDCFhU/CsgjFY3Rj6TQ8TN9I1bqtmO8uygMXXMTQ4CosjhHVKGh1DZPJXik1UXCc0zxKFTXklIimVf9Oa+spNYcjgNVTa0HQSqoDzQGUj2YYmkNSWfjT4Y8D22NDaQeE6+SaaQS9I4+SNx+U00jQxDPPBIdEQSM3qyeLsjfKNVQNsCdo0PowWCLeOa5hjGjvH7Ws0OCoMjlqSDXU6OFJZ2iHDmhudHb/VCnZH3m2W7GH/G8W1w85He4Nu3so8BJ/30jANOGVKsWHDNHFDjzo13tTT0CHkqfnGWDoxIa8z6QVpjX04PA2L0b7oilHGgacuqbEf1Cb3KzVnFF0xLDoSGZ4iRtYL0oq5iNnLOKyHc3k9aGywHrhYfhwr6mjHuW3ti64Y5WQYC2sa2RqCDn4cF7MnTQzrG4iTo18Gm2Hvjprmj/bG1uth4FRjTBkimTE7cULDdtNMwwC8290fqfJLrQ1kBNr0gaIUKDoSsOwWzxtsb4ycuHbZ1mx3ljGUs7bnC8caPV8j13g2XV2fnhtIQU5biylSUorEP+ffTZInbE4S47INSeNiyBSg6xSHgRv2l6IJUYpTxsacKt723VK84dniLwHjgzOGjXHISeigcJY2JuTMluL1fHkrxUknfhzTKsQm3iccTzxr0CeEPepTewuhDvt8d/SDOO76+66fbubdam+W/gpOQj7JSdoBjmg/9OK+3bT9CzAQDk1ZCa0hOBJHiQ/to/jYz+6m/bU476bXzydaqG4g9BPRqRRy+XDYL7vV5xdQcjrO40rOlnFJJy2N7Zeu/7z9y0uwCjdemW4MGftpNd9APXOxmW7atfh4Iw6XbT+fTfdyZcXVAAwBuBrTaCnBNKR95B53KyiVNm/ALkF5Ej3sNamfUYnhkZKq2qZWh5id/LGf385Xb55gcJI4xz0cSPIm10VQmRlr8n7/cPVfDslnyoaUw3s5U0X/45mKWPdYH9E+nykXUh7sZVefKIXF3iPUuXSuoFbFLL/cagjVBmOjUrSJtvqCIh/UENsWgx4sDIQjCB2sxcDMGXzsA/4vadbHoMc+hhaDHiyGPsYWxX8Rq889uy3gcpfdZi0s0nDkj/qHdSvO+vnfsOXE+/mSNuBxt7yfrr6ID9Nlu2eXUH20pxcvcV5l78VoSiiAndwvpqvrveHgaywncXnziF2M+fAmcfSwnq/a9VqcTeHcHberTT9dCPUCMBQrZSVsgIhLXLO5E28XXfeSAUJCCeWgSZtSXtLLu1bgsp4tprOnK4dbFC82e2iKKtIcqyycjBJlvCQPrdvF+iU0maNOdkTSOQgctYvb+cPyObEkQTvYNAqrxeyCGFWoQsnzMd7iJiWMT3AtJIyT/iA6daAhj1XXom2aLVEA0wILAiRZDIDAJCv3YCkP96Io6d6VA02CapDG/tN3e1ZR06WJr6Kle6fJAQcYCg8bLeHpwdH5p4PDDyf7vLE7FIOcyMbyqsFm2wgHa4H10XZyRbPjvLuiZXLeYc4fEng5Z2d9d/2wzzFKBGz1FOMC3v7zDtaupIpfz34Xpz+fnZ9eXIirj+fvT67enZyK317Z+Ns3z/D5gIbQWyJw8imTz6ZflnBexay7bp+zfQrKGnIP7WWtysl4e34pDl8fvxHRBqiakkvPB0LsTKmsngxl2pdtv1yL7kZc9lMYnjg5PHsBUeK9LxNVSeUnl2c0wuecE6jtoBV3q4Lomffs23a6eeihGngtLtr+7/msXe9jWipS9jBl8fEBRv7yInN2eHZ6/h49fHl+SN69+qX2btmgmu6D4wbNutqgRtUpGLoRRoUxAyvr80zO25s3Av6n7dvVrH339Ah9FQe1shoKVjhNOWYfP6w3UCb/NF28ITspxaeLfVC3EyQ1XPsh7vNBwr7ymXpxd78RV7d3e8+RovcCjgoGb2t4Fx9QIUSXKw4NYxKfb58xTQ3RH28sfJ5eal9GNL+nc3MCGXrfqPy+ETFU8nI7IhUPpDmQ/hmDMhDPkqt9GZzNWeBfeFTO5i3sxn3rZKgG5ZEwP07lc/dKlTD4j17X0eENrRTeUdohwUK9MuuWy+56vpm363+LTTu7W3WL7vaL6Hqx7m42j9O+FY+wz0T7P7jQYEVzg1eqDWTnqsR+9hbUYUinypQDPl+J6WzW9ddT2M3icQ4lBPJPqUdxeL2cr+brTb5Nib69fVjQX9eNOJn/3fZr/OdZh/VL/+UF45DbF1ZYoJC3yKYTn5oLsZg+ivu+u5v/gVNsnp5nDTVrcuN5LppqVPAZdKgdvWhCylMWrnODXgxaRYsF/GKw3+o7+Bd6QWCEgHBGIM0IZF8RYMdFRtAKK/yRkDUjkD0nYC5PnODwBYYRSI+EbF8RfMMnAfcWKP8YgDQDoHnVPuL5Y4CEbyIMQJoByJ4TDCY6RoBCs/JE1iMh21cE0yTDCXBT4gCUrD1ZV+0DWo7tLYZxBsiaEcieE6DIVpYTdO2HrEdCtq8IpvKDtbUfsmYAs+OH/I7GAHCgeHuUrLnb9QLUbjGw9o7eOEdA1oxA9pwAppbvZjhzku/mrEdCtq8Inl56RkLAoM4IpBmB7CtCxGvFSIByIHBC1oxA9pzgFb5pMoLGLwOMQHokZPuKQHd+Rgj0Ij8SSDMC2VeEiCl8JATJmoNgbcmStw30GYG11bUXsh4J2b4imNoLUKVXXsiaEcyuF4KrvRA8vs8yAmlGcLteCJG+TIyEVHsha0Yge06AS0jkByqahjuB5Ng+W1ft4W7C1zHSgw4DkGYEsq8IcD/iMSEmfK1hBNKMQPackPBWMQKSwlw4ArIeAWRetTcN30bJYonM2pNm7dG8au/wiY0BIl4QGYA0A5B9RUj4KLQlQEFW5ciiGYHsGUFjFRU5wVU5suhti2JfEXyVI+EO1HjNCaQZwe/kSI0f8TgBvwdzQtYjIdtXBF1FZ6i/qyxZNCPonfissR7RnEDfjRiBNCPYnSypsR6pCLH2RdaMEHbqFSzbE18HqE8kX4esGYHsOUGb2hfaV3my6JGQ7StC4HlS61jlyaIZINR5UmM9wj0B1UVke7poBki7njC6ypQa6wkOQDm2z9ZVe1tVK9o4+u46Akgzgt2pV+BOiF9bRoJVVb1SNCOQPSdgPVIRTO2HrEdCtq8ItvID1BchcgBpBrA7frD5E/IIiLUfsmYAsq8IqfaDk5UfSLL2adcPzlT1isZqgvsh65GQ7StCqDKldqnxFYE0I4SdTKmxHmEALCb4bs56BJB51V5XeVJjLVEBSDOA3smTeCUyFSFW1UrRjOB3qhX6MMe3QqD3xZGQ9UjI9hUBTjn3BFQYnu/nrBmB7CuCw4caRvD4/YYRSDMC2VeEUHkCCwq+kFkzQNjxRFTVfVJHXeXKokdAtq8Ips6V0dXxOWtGMLu5Mvo6V0KFUXkia0bwu7kSSojKE1BiVJ7IeiRke0a4Ge7zu2+wCnsO9PW8PPj48rZ5dfjL0bv374WUwoWAXyykecYrIv7HHFQlq+3XXSON0ttX+3a12ft0+DUQ/lcEdnj18OVLyduuF5t2vZmvbkW3Wux/brP7cPjNvjznKO9VfhP80oizRTtdt+K6E6vuBaODPJTi8EXSxvw+vxHru/n904cP5ROOavvwUfQ/PGTml13lzfa7J+Sd8t3z/XzWrvCr2YI+03Y34p5ewsR8Rf3ja92zH4/x0laWxcSQ8oPO9ycSrvXGOxPhgqKTN+E5D9xY5EByhmRlyg6wMbjsuANxP71twQiGq8QBp/0If/4PWx5djw0KZW5kc3RyZWFtCmVuZG9iagoxMiAwIG9iago8PAovTGVuZ3RoIDcxNTUKL1R5cGUgL1hPYmplY3QKL1N1YnR5cGUgL0ltYWdlCi9Db2xvclNwYWNlIFsvSW5kZXhlZCAvRGV2aWNlUkdCIDI1NSA8RDJFNTlDQkNENzZCRTlGMkNFRkFGQ0YzQUJDRDQ1QzdERTg0RERFQkI1RjRGOUU3QjZENDVFQjBEMTUyRUVGNURCRDhFOEE5Q0RFMTkwRTNFRkMyQzFEQjc3QkJENzZBQUFDRDQ1RThGMUNERjlGQkYyQzZERDgzQjVEMzVFRjNGOEU2RUVGNUQ5RDJFNDlDRERFQkI0QzFEQTc2RDdFN0E4QjBEMDUxQ0NFMThGRTJFRUMxQTVDQTM5RkZGRkZGMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwPl0KL1dpZHRoIDEwMjQKL0JpdHNQZXJDb21wb25lbnQgOAovSGVpZ2h0IDc2OAovRmlsdGVyIC9GbGF0ZURlY29kZQo+PgpzdHJlYW0NCnic7d1ZQ+PGtoBRT2CMgSSd8Qyh//+/vHAIt7GRjS3tKqm21nrMg+L25pM1lOXv3wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgElbLfb3Y7+GYraPi8f12C8Cpupx9/ximXMPsH54/cc9P4z9OmCabp/fbFLuAJb//Ouexn4hMEXrzT+FPC/HfikFvO/cnp9vx34pMEE/CkmYyI+d2/Pd2K8FJmjxo//d2K8l3Id/3PPYrwUm6GMi+7FfTLDtRv9wzv5DIptk98luPvzbHP/DZ9sPjTwvxn41oRL/0yDIw8dKtmO/mkjLD/+wXbJDG4ix/niSfDP2qwm0+rhjy3dvA0J8vAL4nGgR0MHH/9gvBqZq9yGUPIuAbj/u1lZjvxqYqpyl5NyrQbi7hPfJFil3ahAv4ZWytFc1IVy+O2UHH/+p7mpCtPuPtWRYKWPpD1zu40rZDKuAs/17oKSDb8q0/7CcdMczUFSu82VLf+Aaqa6XJ7yfAUV9/B5w6/fLPy79ybKeAYrKs14u53pGKOmgmsexX80A6zx7Mqgmy0WzXJcyoY4kV81SXcmEaj4eALS7aObj84w2Pv7hQikWzab4R8AIMqyafUrwb4AxJDh1PriIke33DKCo9i+dZ7mJAfUdHAC0eOv8Mcc9DBhF60vnLP2BAdoOqPXdF4yr6QPog9OXp7FfDbSn5Qto7V++hHE1fANt2/7tSxhZ/CKg9Wq1Wry4WR57ev3Pj6tVzGd1huVLMK6wBbTb1e3iYfnxfOKcu+XNYr8a8vODVv7CcMN/EPwl/OXdcz+75cN+1evDO8fXl2BcQ1YBbx8Xy91x0j1slg+3Vx4LJPn6Moys3w+Cr1/S3zyHWj48Xn780fKNC5iO6x+gtb296Xu8/5Xd0/6iXZClPxDjqpbWtzcRR/znbJ72Xx4HtL1wESbk4h8EXz2U+tw/trt5PHdNL9Pjy2FcF11Le/ngDz7f/8rpw4AETy6AyfjyB8HXt0/HdVZx170LsPIX4pz/Ac2x4n/TsQs4WPrT/s+XwsjOrKUdNf43d7frS18tcLVTn6j3tc/5T3n68BNFfu4bYnWdUa/3pe/0XWO3eD8PsPQHYn2+on5/c1zg6N4OAqz8hWhHd9Rva93ov85usb58tQJwqY/H+ruJnPV32Bx8w9jSHwhxe6q4CbPyF4Jc+uiOCRny/BDgg9XXvU2Mlb8QprUDAD/3DWG2rfW/c/UPYmynd7v/a0t7ABhu/fB1bJP05BwABlpM937/lx58AQgGWE1pmf/1Nm39bBFMSXOX/T67cxkA+lgvvs6rATdOAuBqjR/6/+AkAK60Hv/RPnGWFgPDFR4bvurfxaOA4FKpPvzf3DkEgItk+/B/4xAAvpbww/+NQwD4yirlh///uBEA57W62P8yT9YCwEnbaT7aM87GckA4IeeFv0MuA0Kn3Mf+75bOAeCTdfZj/3c79wHgyP0Mjv3f+WUgONDiI/7783Rg+KDFJ/wN4SIAvFu3/5yPa1kMCG/S3/XvsrEDgO/zuvL3kauAMItFP918HYDZm9eF/0NuAzBzc87fDoCZm9t9v2PuAzJjc8//+fnODoC5kr8dALMl/1d2AMyS/N/YATBD8n9nB8DsyP8HOwBmRv4f2QEwK/I/dDf2QKCe/di9TY6VgMzGvBf9drMDYCYex25tkjwYnFmY6/f9v+J5AMzAVv4nPI49GihtNo/5v55HgpHe/B71ebmdZQDk5sb/OZYBkJo7f+e5C0hi92P3NXmeCUpaa5f+v+QaIFkNu/b3208vfgvKrIz/vL7EnwZtYuMaIDk9DMjiX39/+2crf//686DAivnpr9//eYl//HfAS1yONR4oacCy319//7ihb3/231IxP/1y8I/9d/89gIXAJNR/3d/Pvxxv64+pnQb8/O/jl/jtX703tqozEKio98n/b98+b2xAXSX8/EfHP/jXvltzCYB0Fn1r6Mr/xbCrbLE68x+wA3AJgGR63/n/+ffuDX6b0CnAp/OTf/Q+SLEKgFTWu74p/H1qk3/03WK4/556id/6XgT0TSBS6X3r76fT2+x9eB3s5+4TlFd/9d2mLwKQyKp3XKcOrV/83nujsf488w//T9+NuglIGv2P/v9zbrMTuQdw+uP/+/f+KxWcAZBF/4V/J0+tX/U+ug515gxlyDGKMwCS6H/0f+7wfyonAOcO/79/twyQuet99H/+2Pr79/7bDXR2FzVgmcJmGzkDGEnvlT8vzm95EmuAzvf/3/4btgqIBLZD4jq/6Qb6H/JVJc8Dpn2DvvR/ftPJ+/c4UJo37Md+zm97EkuAz/c/6B6lS4C0bsDFvxfdX6x5N2jTUf46+xKHHaK4BEjbhlz8+yquaXwD4OwShYG7qKfQWUBlQ5/4+eu5jf972LaDnF2i+PfAjXsUCC0b+nMfZ75bM2BxfaxzFwCGfkfJKkAaNuje3/+cWV33y+CNxzhzjDJ8haIfBaZdT4P//s8cAEzi6v+r0wcAw7+ivCsxFqhhwML///evUxufzkOAfzv1EiOOUBwA0KqQ3/o9cQtgGhf/35w4A/g94ocKPAyURkV8/D+f2AH8MakfAencAQQ9odAiINoU8vH/3LkD+HtS+XfuAKJ+o8ABAE0K+vh/8a+ji4DfBnyrrpDfjlcqDvgBoCMOAGhR1Mf/i5///PAU8G9xaUU6+I2yvwLXJmyqTw4G6/3E/24//fnLS2C///LviTz1r8Nvf/7ychTw7Ze/gn+h1C0A2jN06R/vrAGgOcOX/vHOAQCt6f/MX455EhiNGfrFPz7yNUDash87mVRuxh4nXGXYY3844kFAtCRu7Q+vrAGiJcO/+MtHbgHSEDf/ovktANrh6l80TwKlHa7+hXMFkFYEL/3nxX7socKFLP2P50nAtMLavwLux54qXGTYT/7R7WHsscJFHP6XYAkAbXD4X4QTAFrg8L8MJwC0wOF/GU4AaIHD/0KcADB9Dv9LsQSI6fPgr1IsAWL6rP0vxi8BMXW++luO5wAzdb76W47HADJ1nvxTjl8CY+rc/SvIHUCmzVf/S3IHkGlz+l+Sp4AxbU7/S3IBgGlz+l+UpwAyZe7+l2UFAFN2O3YgyfkOMFNm8X9ZvgLAlC3HDiS7sQcMZ4ydR3qrsScMJ1n9U5oVQEyXy3+luQDIdC3GziO95dgjhpNc/ivNCkCmy7N/ivMMICZr7DhmwA0Apsrl//LcAGCqVmPHMQOLsYcMJ7j8X54bAEyV1f/l6Z+pcvuvgrGHDCfov4KxhwwnjJ3GLHgGMBM1dhqzYAEA0+ThXzU8jj1m6OT2fw0WADBN+q9B/0yTb//X4AkATJPlfzVYAMQ06b8G/TNN+q9B/0zTzdhpzIL+mSbLf2vwBDCmSf9VjD1m6KT/KsYeM3TSfxVjjxk66b+KsccMnfRfxdhjhk4PSyoYe8wAAAAAAAA0Y7V/WL7fxb9bPixWfm26cfe3i+Vy8zbR3fJmsdqO/YqYpvtF1/qduwfPm27Vdv+0+TzR3Y0nCHNku9idXG+2efCTE+1Z7+9OT/TGTp0fVk9fLDld3o79ErnK9qtnsexMlDerS9bt7xw0tuPL+v93ELAf+2UyAduvPvvfLZ0FtGF96XMYd84CZm/fcYXoFL880YLV6Ss5nzy5vzNr6+u+snt3/hBgtaCC8xN9uGqiG6d1M7a64sP/7c/l7FUjz/+u4twItqcv+p/goG62+vxe17lfn9J/FWcmcH/tDv3ZOcBs9cv1JnqDXOn0APr9AOOdHcAc9f21jtM7AP1XcfL97/v7q3YAM3TddaKPTu4A9F/Fqbe//88v2wHMzpDf6j61A9B/FSfe/fsBm7QDmJnHQX+BJ1aO6b+K7je/z6W/H85c1SGf7aA/lufn7oVj+q+i871fX33j75DFwHMy8I/ledN5vKj/KjonOvi3l63uno/hoT6V2SwX6Hrrh53Pvdq5BDAXQ64UvetaN6r/Kjre+fXA87lX51Z2kUnE73R1nQHov4qOifa/mfuBM4B5GHLr74eOdeP6r+LzG78N2a4fFpuHK74fes7nB0nqv4rPEw364VWPA5iDmI//rlvG+q/i0/secT3nlQOAOQj6+O+4AqD/Kj5NdPC9v3cOAPIbfqfo3aclI/qv4vhtjzn7f2UVYH6XPu7va7vjTeu/ioJvuzUA2a3j/lg+3TDSfxXHI406oXu2Cji/qKt/r45XjOi/iqN3Perq36u7Wn+GjCTu8P/zCYD+qyj5rvt1wOQi/1iOTwD0X8XRRId+l+uAXwXKbRX5x3J8uqj/Kg7f9MgLOu4AZBeb6E3JjXPC4Zseu0f/dE+HVCJP/z9dLtJ/FUXf9Hp/iowg9GSx8J8i3Q7f9LDFf28sAUwt9o/l6AKg/qs4nGjQd3/euQCYWdxS0TeHnxb6r+JwpAFP/vjIz4FlFnux6PjTQv9VHI40eOP6zyy6/8O/Fv1XcTjS4I13PtiRJPSfwOFIgzfuGQCZ6T+Bg/c8cvX/K/1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Eyg6Uf1npv8Eik5U/5npP4GiE9V/ZvpPoOhE9Z+Z/hMoOlH9Z6b/BIpOVP+Z6T+BohPVf2b6T6DoRPWfmf4TKDpR/Wem/wSKTlT/mek/gaIT1X9m+k+g6ET1n5n+Ezh4z9fBG9d/ZvpP4HCkwRvXf2b6T+BwpMEbf6j410ht0f2vDrau/yoORxq88cM9OskE/7XofwSHE72L3bj+U9vE/rWsDzau/yoOJ7qM3fjhHp1kYv9aNocb138VRd/0bb2/Rep7CP1jObpYrP8qDt/029BtH+3RSSb2r+XoZFH/VRy+6feh23b7L7dt6F/L4+HG9V/F0UhDL+m4/JfcLvKv5fDyn/7rOJroTeS276v9ITKKyAsAT0fb1n8VR+965Cmd0//sIk8Xb4+2rf8qjt71yG8A3NT6M2QsgScAR4f/+q/jeKJPcZt29z+9uEg/fVjov4rjt/0xbMu7On+CjCjuDsCnDwv9V/FppGGHdPsqf4GMKup68edbxfqv4tP7HnUFcHN8QkdCUQcAj5+2rP8qPo806ADAzf9ZiDkA6Fgppv8qPr/xMQcAPv7nYRuyYqzjUrH+q+gYacgBgLP/mYjotOtOsf6r6HjnI57rclf6z46pGP7MiM5jRf1X0TXRgDUA7v3PxvBFgJ8v/n3XfyVdb/168BmAi38zsh/4x9K9TlT/VXS+90PPABz9z8qwewB33VeK9V9F90SH7dI3nvszK+shlwBO3SjSfxUnRjpol+57vzMzYAewOfXHov8qTo10wLMdj7/JSXr3fVcBnMxf/3Wcevv779LlP0P3/S4Zn85f/3WcfP/77gDkP0u9/lzO5K//Os6MtM81gI38Z2p9/aqRE1f+3+i/inMjvf7xbud26CR3bbE3Z78iov8qzk708crLOktf+pmzqy4CbDpX/f2g/yrOD2F71W0Aq/5mbn15tOc//L/rv5KvRrq/+BBg6difCz8wll9/P0T/VXw5h/VlVwF2LvzxavX1HuCC+vVfyQWT2H59I0D9/L/tzdljxpvLvhuq/youmsV6cfbKzlL9HLg9tQt4ur30ErH+q7h0oo83J3YBd3vf9uGz1eLp8C9ms1xc81QI/VdxxUTu9zdHi7yWD4/u+HHaanW7eLVfXf1AGP1Xce1Y7v+Z6GK1kj7l6L+KsccMnfRfxdhjhk76r2LsMUMn/Vcx9pihk/6rGHvM0En/VYw9Zuik/yrGHjN00n8VY48ZOum/irHHDJ30X8XYY4ZO+q9i7DFDJ/1XMfaYoZP+qxh7zNBJ/1WMPWbopP8qxh4zdNJ/FWOPGTrpv4qxxwyd9F/F2GOGTvqvYuwxQyf9VzH2mKGT/mtYjj1m6HQ7dhqzoH+maTV2GrOgf6ZJ/zU8jT1m6LQeO41ZWIw9Zug2dhqzsB97ytDt698RZ7Crf5YN6vj6V+cZzG/4MVH7sduYgd3YQ4YT7seOYwZuxh4ynLIZu478bseeMZziAkBxTv+ZrMex60jvbuwRw2lOAApz958JcwJQmMN/JswdgLJc/WfS7sYuJDeL/5g0zwAoyXd/mbjd2I1k5uOfiXMAUI6PfybPlwCL8fHP5HkKUCku/tOAh7E7SWrj3j8NWLsEWMTj2IOFSzgDKMFzP2mEHwKJt3P0Tyuexq4lnc392DOFS60tAw7m5J+G2AHE8tQfmnLvSQCB5E9j7t0FDCN/muMUIIr8adDaXYAIG6v+aZN1AMPdufFHq1auAg70YNkP7XIOMMjOsT9tW7kP0NvChz/N2zsJ6OVmO/bkIMB67xjgauonj1vXAa6xW6ifVLZ7Dwa8zO7BLT8SWj8+2Aecd3ezFz+JbVe3i69EV/Xl/7BL9K7q5sv/40r68P17cHnPvV5E9F7IrXy4SHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4YEl6d/aEhwefqHhgSXp39oSHB5+oeGBJenf2hIcHn6h4Ysg9Pr9SL0D6MI7n/Z60XoH0Yxif73sS+i30EIzM9DbHlPvV7EKvZF6B8uE3zovej1Iu5jX8Rd8HsEWQV/9N72exWxL6LfQQjMzzY2vft+ryL2KkS/gxCYoU1oej1fROxVCJf/4UJPkeX1u/z//ftt5It4Xoe+QZBYaHr7ni9iHfkiXP6DS4VeANj2fRV3gS+i704IZigwvV3vFxG5Aqj3TgjmJzC9/p+8gUchDv/hcoHn3gM+eeMuQ/ZcggDzdBNV3s2AF/EY9SI2rv7DFcKOvQfdd98FvQiLf+AqQQcAfW/+vwm6D+njH64TdAAwcNldzAGAj3+4UsgBwJCz/1ch30Ty8Q/XWgd8CWAz+LZ7xC0AF//hagFX34evutsO3wsNuwQBMzX4szeivMGXAIcfg8AcrQdefYs57x56HeIx4kXA/Ax8BFfMV+7Xw76L8BDyImCGBh18R112G3QJwMk/9Dbge0BDb/39cN9/B3Dn1h/01/vsOy7/ATsA+cMgPZ/CF5l/7x2A/GGgXtcAolfc9toByB8Ge7y6vU38irvt9XcBYg9BYKbur2zvrucD/89aX3klosA+CObpqosAD4UOu686DimyD4J5Wl18CLAr90Mblx8CbHzjFyLdXvTpWzi81WW/CXZjyT/EWi++3ANsFsWvuF+wB1A/lHB79izgrs4Vt9XZs4DdQv1QyHZ/Yhdw91Cvu/XtiW8mb2582Q+KWj8+HB2DLx9uq3/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
+
+
+
+
+
+"""