karrio-canadapost 2025.5rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- karrio/mappers/canadapost/__init__.py +3 -0
- karrio/mappers/canadapost/mapper.py +88 -0
- karrio/mappers/canadapost/proxy.py +373 -0
- karrio/mappers/canadapost/settings.py +23 -0
- karrio/plugins/canadapost/__init__.py +23 -0
- karrio/providers/canadapost/__init__.py +25 -0
- karrio/providers/canadapost/error.py +42 -0
- karrio/providers/canadapost/manifest.py +127 -0
- karrio/providers/canadapost/pickup/__init__.py +3 -0
- karrio/providers/canadapost/pickup/cancel.py +33 -0
- karrio/providers/canadapost/pickup/create.py +217 -0
- karrio/providers/canadapost/pickup/update.py +55 -0
- karrio/providers/canadapost/rate.py +192 -0
- karrio/providers/canadapost/shipment/__init__.py +8 -0
- karrio/providers/canadapost/shipment/cancel.py +53 -0
- karrio/providers/canadapost/shipment/create.py +308 -0
- karrio/providers/canadapost/tracking.py +75 -0
- karrio/providers/canadapost/units.py +285 -0
- karrio/providers/canadapost/utils.py +92 -0
- karrio/schemas/canadapost/__init__.py +0 -0
- karrio/schemas/canadapost/authreturn.py +3389 -0
- karrio/schemas/canadapost/common.py +2037 -0
- karrio/schemas/canadapost/customerinfo.py +2307 -0
- karrio/schemas/canadapost/discovery.py +3016 -0
- karrio/schemas/canadapost/manifest.py +3704 -0
- karrio/schemas/canadapost/merchantregistration.py +1498 -0
- karrio/schemas/canadapost/messages.py +1431 -0
- karrio/schemas/canadapost/ncshipment.py +7231 -0
- karrio/schemas/canadapost/openreturn.py +2438 -0
- karrio/schemas/canadapost/pickup.py +1407 -0
- karrio/schemas/canadapost/pickuprequest.py +6794 -0
- karrio/schemas/canadapost/postoffice.py +2240 -0
- karrio/schemas/canadapost/rating.py +5308 -0
- karrio/schemas/canadapost/serviceinfo.py +1505 -0
- karrio/schemas/canadapost/shipment.py +9982 -0
- karrio/schemas/canadapost/track.py +3100 -0
- karrio_canadapost-2025.5rc1.dist-info/METADATA +44 -0
- karrio_canadapost-2025.5rc1.dist-info/RECORD +41 -0
- karrio_canadapost-2025.5rc1.dist-info/WHEEL +5 -0
- karrio_canadapost-2025.5rc1.dist-info/entry_points.txt +2 -0
- karrio_canadapost-2025.5rc1.dist-info/top_level.txt +3 -0
@@ -0,0 +1,127 @@
|
|
1
|
+
import karrio.schemas.canadapost.manifest as canadapost
|
2
|
+
import typing
|
3
|
+
import karrio.lib as lib
|
4
|
+
import karrio.core.models as models
|
5
|
+
import karrio.providers.canadapost.error as error
|
6
|
+
import karrio.providers.canadapost.utils as provider_utils
|
7
|
+
import karrio.providers.canadapost.units as provider_units
|
8
|
+
|
9
|
+
|
10
|
+
def parse_manifest_response(
|
11
|
+
_response: lib.Deserializable[lib.Element],
|
12
|
+
settings: provider_utils.Settings,
|
13
|
+
) -> typing.Tuple[models.ManifestDetails, typing.List[models.Message]]:
|
14
|
+
response = _response.deserialize()
|
15
|
+
links = lib.find_element("link", response)
|
16
|
+
|
17
|
+
messages = error.parse_error_response(response, settings)
|
18
|
+
details = (
|
19
|
+
_extract_details(links, settings, _response.ctx) if len(links) > 0 else None
|
20
|
+
)
|
21
|
+
|
22
|
+
return details, messages
|
23
|
+
|
24
|
+
|
25
|
+
def _extract_details(
|
26
|
+
links: typing.List[lib.Element],
|
27
|
+
settings: provider_utils.Settings,
|
28
|
+
ctx: dict = None,
|
29
|
+
) -> models.ManifestDetails:
|
30
|
+
manifest = lib.bundle_base64(ctx["files"])
|
31
|
+
|
32
|
+
return models.ManifestDetails(
|
33
|
+
carrier_id=settings.carrier_id,
|
34
|
+
carrier_name=settings.carrier_id,
|
35
|
+
doc=models.ManifestDocument(manifest=manifest),
|
36
|
+
meta=dict(
|
37
|
+
group_ids=ctx["group_ids"],
|
38
|
+
links=[_.get("href") for _ in links],
|
39
|
+
),
|
40
|
+
)
|
41
|
+
|
42
|
+
|
43
|
+
def manifest_request(
|
44
|
+
payload: models.ManifestRequest,
|
45
|
+
settings: provider_utils.Settings,
|
46
|
+
) -> lib.Serializable:
|
47
|
+
address = lib.to_address(payload.address)
|
48
|
+
options = lib.units.Options(
|
49
|
+
payload.options,
|
50
|
+
option_type=lib.units.create_enum(
|
51
|
+
"ManifestOptions",
|
52
|
+
{
|
53
|
+
"group_ids": lib.OptionEnum("group_ids", list),
|
54
|
+
"shipments": lib.OptionEnum("shipments", lib.to_dict),
|
55
|
+
"method_of_payment": lib.OptionEnum("method_of_payment"),
|
56
|
+
"shipping_point_id": lib.OptionEnum("shipping_point_id"),
|
57
|
+
"excluded_shipments": lib.OptionEnum("excluded_shipments", list),
|
58
|
+
"detailed_manifests": lib.OptionEnum("detailed_manifests", bool),
|
59
|
+
"cpc_pickup_indicator": lib.OptionEnum("cpc_pickup_indicator", bool),
|
60
|
+
"requested_shipping_point": lib.OptionEnum("requested_shipping_point"),
|
61
|
+
},
|
62
|
+
),
|
63
|
+
)
|
64
|
+
group_ids = lib.identity(
|
65
|
+
options.group_ids.state
|
66
|
+
or [
|
67
|
+
*set(
|
68
|
+
(
|
69
|
+
_.get("meta", {}).get("group_id")
|
70
|
+
for _ in (options.shipments.state or [])
|
71
|
+
if lib.text(_.get("meta", {}).get("group_id")) is not None
|
72
|
+
)
|
73
|
+
)
|
74
|
+
]
|
75
|
+
)
|
76
|
+
retrieve_shipments = len(group_ids) == 0
|
77
|
+
|
78
|
+
request = canadapost.ShipmentTransmitSetType(
|
79
|
+
customer_request_id=None,
|
80
|
+
group_ids=canadapost.GroupIDListType(group_id=["[GROUP_IDS]"]),
|
81
|
+
cpc_pickup_indicator=options.cpc_pickup_indicator.state,
|
82
|
+
requested_shipping_point=provider_utils.format_ca_postal_code(
|
83
|
+
options.requested_shipping_point.state or address.postal_code
|
84
|
+
),
|
85
|
+
shipping_point_id=options.shipping_point_id.state,
|
86
|
+
detailed_manifests=lib.identity(
|
87
|
+
True
|
88
|
+
if options.detailed_manifests.state is not False
|
89
|
+
else options.detailed_manifests.state
|
90
|
+
),
|
91
|
+
method_of_payment=(options.method_of_payment.state or "Account"),
|
92
|
+
manifest_address=canadapost.ManifestAddressType(
|
93
|
+
manifest_name=address.contact,
|
94
|
+
manifest_company=address.company_name or address.contact or "N/A",
|
95
|
+
phone_number=address.phone_number or "000 000 0000",
|
96
|
+
address_details=canadapost.AddressDetailsType(
|
97
|
+
address_line_1=address.address_line1,
|
98
|
+
address_line_2=address.address_line2,
|
99
|
+
city=address.city,
|
100
|
+
prov_state=address.state_code,
|
101
|
+
country_code=address.country_code,
|
102
|
+
postal_zip_code=address.postal_code,
|
103
|
+
),
|
104
|
+
),
|
105
|
+
customer_reference=lib.text(payload.reference, max=12),
|
106
|
+
excluded_shipments=lib.identity(
|
107
|
+
canadapost.ExcludedShipmentsType(
|
108
|
+
shipment_id=options.excluded_shipments.state.slit(",")
|
109
|
+
)
|
110
|
+
if options.excluded_shipments.state
|
111
|
+
else None
|
112
|
+
),
|
113
|
+
)
|
114
|
+
|
115
|
+
return lib.Serializable(
|
116
|
+
request,
|
117
|
+
lambda _: lib.to_xml(
|
118
|
+
request,
|
119
|
+
name_="transmit-set",
|
120
|
+
namespacedef_='xmlns="http://www.canadapost.ca/ws/manifest-v8"',
|
121
|
+
),
|
122
|
+
dict(
|
123
|
+
group_ids=group_ids,
|
124
|
+
retrieve_shipments=retrieve_shipments,
|
125
|
+
shipment_identifiers=payload.shipment_identifiers,
|
126
|
+
),
|
127
|
+
)
|
@@ -0,0 +1,3 @@
|
|
1
|
+
from karrio.providers.canadapost.pickup.create import parse_pickup_response, pickup_request
|
2
|
+
from karrio.providers.canadapost.pickup.update import parse_pickup_update_response, pickup_update_request
|
3
|
+
from karrio.providers.canadapost.pickup.cancel import parse_pickup_cancel_response, pickup_cancel_request
|
@@ -0,0 +1,33 @@
|
|
1
|
+
from typing import Tuple, List
|
2
|
+
from karrio.core.models import (
|
3
|
+
PickupCancelRequest,
|
4
|
+
Message,
|
5
|
+
ConfirmationDetails,
|
6
|
+
)
|
7
|
+
from karrio.core.utils import Serializable, Element
|
8
|
+
from karrio.providers.canadapost.error import parse_error_response
|
9
|
+
from karrio.providers.canadapost.utils import Settings
|
10
|
+
import karrio.lib as lib
|
11
|
+
|
12
|
+
|
13
|
+
def parse_pickup_cancel_response(
|
14
|
+
_response: lib.Deserializable[Element], settings: Settings
|
15
|
+
) -> Tuple[ConfirmationDetails, List[Message]]:
|
16
|
+
response = _response.deserialize()
|
17
|
+
errors = parse_error_response(response, settings)
|
18
|
+
cancellation = (
|
19
|
+
ConfirmationDetails(
|
20
|
+
carrier_id=settings.carrier_id,
|
21
|
+
carrier_name=settings.carrier_name,
|
22
|
+
success=True,
|
23
|
+
operation="Cancel Pickup",
|
24
|
+
)
|
25
|
+
if len(errors) == 0
|
26
|
+
else None
|
27
|
+
)
|
28
|
+
|
29
|
+
return cancellation, errors
|
30
|
+
|
31
|
+
|
32
|
+
def pickup_cancel_request(payload: PickupCancelRequest, _) -> Serializable:
|
33
|
+
return Serializable(payload.confirmation_number)
|
@@ -0,0 +1,217 @@
|
|
1
|
+
from typing import Tuple, List, Union
|
2
|
+
from functools import partial
|
3
|
+
from karrio.schemas.canadapost.pickup import pickup_availability
|
4
|
+
from karrio.schemas.canadapost.pickuprequest import (
|
5
|
+
PickupRequestDetailsType,
|
6
|
+
PickupRequestUpdateDetailsType,
|
7
|
+
PickupLocationType,
|
8
|
+
AlternateAddressType,
|
9
|
+
ContactInfoType,
|
10
|
+
LocationDetailsType,
|
11
|
+
ItemsCharacteristicsType,
|
12
|
+
PickupTimesType,
|
13
|
+
OnDemandPickupTimeType,
|
14
|
+
PickupRequestPriceType,
|
15
|
+
PickupRequestHeaderType,
|
16
|
+
PickupTypeType as PickupType,
|
17
|
+
)
|
18
|
+
import karrio.lib as lib
|
19
|
+
from karrio.core.utils import (
|
20
|
+
Serializable,
|
21
|
+
Element,
|
22
|
+
Job,
|
23
|
+
Pipeline,
|
24
|
+
DF,
|
25
|
+
NF,
|
26
|
+
XP,
|
27
|
+
)
|
28
|
+
from karrio.core.models import (
|
29
|
+
PickupRequest,
|
30
|
+
PickupDetails,
|
31
|
+
Message,
|
32
|
+
ChargeDetails,
|
33
|
+
PickupUpdateRequest,
|
34
|
+
)
|
35
|
+
from karrio.core.units import Packages
|
36
|
+
from karrio.providers.canadapost.units import PackagePresets
|
37
|
+
from karrio.providers.canadapost.utils import Settings
|
38
|
+
from karrio.providers.canadapost.error import parse_error_response
|
39
|
+
|
40
|
+
PickupRequestDetails = Union[PickupRequestDetailsType, PickupRequestUpdateDetailsType]
|
41
|
+
|
42
|
+
|
43
|
+
def parse_pickup_response(
|
44
|
+
_response: lib.Deserializable[Element], settings: Settings
|
45
|
+
) -> Tuple[PickupDetails, List[Message]]:
|
46
|
+
response = (
|
47
|
+
_response.deserialize() if hasattr(_response, "deserialize") else _response
|
48
|
+
)
|
49
|
+
pickup = (
|
50
|
+
_extract_pickup_details(response, settings)
|
51
|
+
if len(lib.find_element("pickup-request-header", response)) > 0
|
52
|
+
else None
|
53
|
+
)
|
54
|
+
return pickup, parse_error_response(response, settings)
|
55
|
+
|
56
|
+
|
57
|
+
def _extract_pickup_details(response: Element, settings: Settings) -> PickupDetails:
|
58
|
+
header = lib.find_element(
|
59
|
+
"pickup-request-header", response, PickupRequestHeaderType, first=True
|
60
|
+
)
|
61
|
+
price = lib.find_element(
|
62
|
+
"pickup-request-price", response, PickupRequestPriceType, first=True
|
63
|
+
)
|
64
|
+
price_amount = (
|
65
|
+
sum(
|
66
|
+
[
|
67
|
+
NF.decimal(price.hst_amount or 0.0),
|
68
|
+
NF.decimal(price.gst_amount or 0.0),
|
69
|
+
NF.decimal(price.due_amount or 0.0),
|
70
|
+
],
|
71
|
+
0.0,
|
72
|
+
)
|
73
|
+
if price is not None
|
74
|
+
else None
|
75
|
+
)
|
76
|
+
|
77
|
+
return PickupDetails(
|
78
|
+
carrier_id=settings.carrier_id,
|
79
|
+
carrier_name=settings.carrier_name,
|
80
|
+
confirmation_number=header.request_id,
|
81
|
+
pickup_date=DF.fdate(header.next_pickup_date),
|
82
|
+
pickup_charge=ChargeDetails(
|
83
|
+
name="Pickup fees", amount=NF.decimal(price_amount), currency="CAD"
|
84
|
+
)
|
85
|
+
if price is not None
|
86
|
+
else None,
|
87
|
+
)
|
88
|
+
|
89
|
+
|
90
|
+
def pickup_request(payload: PickupRequest, settings: Settings) -> Serializable:
|
91
|
+
request: Pipeline = Pipeline(
|
92
|
+
get_availability=lambda *_: _get_pickup_availability(payload),
|
93
|
+
create_pickup=partial(_create_pickup, payload=payload, settings=settings),
|
94
|
+
)
|
95
|
+
return Serializable(request)
|
96
|
+
|
97
|
+
|
98
|
+
def _create_pickup_request(
|
99
|
+
payload: PickupRequest, settings: Settings, update: bool = False
|
100
|
+
) -> Serializable:
|
101
|
+
"""
|
102
|
+
pickup_request create a serializable typed PickupRequestDetailsType
|
103
|
+
|
104
|
+
Options:
|
105
|
+
- five_ton_flag
|
106
|
+
- loading_dock_flag
|
107
|
+
|
108
|
+
:param update: bool
|
109
|
+
:param payload: PickupRequest
|
110
|
+
:param settings: Settings
|
111
|
+
:return: Serializable
|
112
|
+
"""
|
113
|
+
RequestType = PickupRequestUpdateDetailsType if update else PickupRequestDetailsType
|
114
|
+
packages = Packages(payload.parcels, PackagePresets, required=["weight"])
|
115
|
+
heavy = any([p for p in packages if p.weight.KG > 23])
|
116
|
+
location_details = dict(
|
117
|
+
instruction=payload.instruction,
|
118
|
+
five_ton_flag=payload.options.get("five_ton_flag"),
|
119
|
+
loading_dock_flag=payload.options.get("loading_dock_flag"),
|
120
|
+
)
|
121
|
+
address = lib.to_address(payload.address)
|
122
|
+
|
123
|
+
request = RequestType(
|
124
|
+
customer_request_id=settings.customer_number,
|
125
|
+
pickup_type=PickupType.ON_DEMAND.value,
|
126
|
+
pickup_location=PickupLocationType(
|
127
|
+
business_address_flag=(not payload.address.residential),
|
128
|
+
alternate_address=AlternateAddressType(
|
129
|
+
company=address.company_name or "",
|
130
|
+
address_line_1=address.address_line,
|
131
|
+
city=address.city,
|
132
|
+
province=address.state_code,
|
133
|
+
postal_code=address.postal_code,
|
134
|
+
)
|
135
|
+
if payload.address
|
136
|
+
else None,
|
137
|
+
),
|
138
|
+
contact_info=ContactInfoType(
|
139
|
+
contact_name=payload.address.person_name,
|
140
|
+
email=payload.address.email or "",
|
141
|
+
contact_phone=payload.address.phone_number,
|
142
|
+
telephone_ext=None,
|
143
|
+
receive_email_updates_flag=(payload.address.email is not None),
|
144
|
+
),
|
145
|
+
location_details=(
|
146
|
+
LocationDetailsType(
|
147
|
+
five_ton_flag=location_details["five_ton_flag"],
|
148
|
+
loading_dock_flag=location_details["loading_dock_flag"],
|
149
|
+
pickup_instructions=location_details["instruction"],
|
150
|
+
)
|
151
|
+
if any(location_details.values())
|
152
|
+
else None
|
153
|
+
),
|
154
|
+
items_characteristics=(
|
155
|
+
ItemsCharacteristicsType(
|
156
|
+
pww_flag=None,
|
157
|
+
priority_flag=None,
|
158
|
+
returns_flag=None,
|
159
|
+
heavy_item_flag=heavy,
|
160
|
+
)
|
161
|
+
if heavy
|
162
|
+
else None
|
163
|
+
),
|
164
|
+
pickup_volume=f"{len(packages) or 1}",
|
165
|
+
pickup_times=PickupTimesType(
|
166
|
+
on_demand_pickup_time=OnDemandPickupTimeType(
|
167
|
+
date=payload.pickup_date,
|
168
|
+
preferred_time=payload.ready_time,
|
169
|
+
closing_time=payload.closing_time,
|
170
|
+
),
|
171
|
+
scheduled_pickup_times=None,
|
172
|
+
),
|
173
|
+
payment_info=None,
|
174
|
+
)
|
175
|
+
return Serializable(request, partial(_request_serializer, update=update))
|
176
|
+
|
177
|
+
|
178
|
+
def _get_pickup_availability(payload: PickupRequest):
|
179
|
+
return Job(
|
180
|
+
id="availability", data=(payload.address.postal_code or "").replace(" ", "")
|
181
|
+
)
|
182
|
+
|
183
|
+
|
184
|
+
def _create_pickup(
|
185
|
+
availability_response: str, payload: PickupRequest, settings: Settings
|
186
|
+
):
|
187
|
+
availability = XP.to_object(pickup_availability, XP.to_xml(availability_response))
|
188
|
+
data = (
|
189
|
+
_create_pickup_request(payload, settings)
|
190
|
+
if availability.on_demand_tour
|
191
|
+
else None
|
192
|
+
)
|
193
|
+
|
194
|
+
return Job(id="create_pickup", data=data, fallback="" if data is None else "")
|
195
|
+
|
196
|
+
|
197
|
+
def _get_pickup(
|
198
|
+
update_response: str, payload: PickupUpdateRequest, settings: Settings
|
199
|
+
) -> Job:
|
200
|
+
errors = parse_error_response(XP.to_xml(XP.bundle_xml([update_response])), settings)
|
201
|
+
data = (
|
202
|
+
None
|
203
|
+
if any(errors)
|
204
|
+
else f"/enab/{settings.customer_number}/pickuprequest/{payload.confirmation_number}/details"
|
205
|
+
)
|
206
|
+
|
207
|
+
return Job(
|
208
|
+
id="get_pickup", data=Serializable(data), fallback="" if data is None else ""
|
209
|
+
)
|
210
|
+
|
211
|
+
|
212
|
+
def _request_serializer(request: PickupRequestDetails, update: bool = False) -> str:
|
213
|
+
return XP.export(
|
214
|
+
request,
|
215
|
+
name_=("pickup-request-update" if update else "pickup-request-details"),
|
216
|
+
namespacedef_='xmlns="http://www.canadapost.ca/ws/pickuprequest"',
|
217
|
+
)
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import karrio.lib as lib
|
2
|
+
from typing import cast, Tuple, List
|
3
|
+
from functools import partial
|
4
|
+
from karrio.core.utils import Job, Pipeline, Serializable, Element
|
5
|
+
from karrio.core.models import (
|
6
|
+
PickupRequest,
|
7
|
+
PickupUpdateRequest,
|
8
|
+
PickupDetails,
|
9
|
+
Message,
|
10
|
+
)
|
11
|
+
|
12
|
+
from karrio.providers.canadapost.utils import Settings
|
13
|
+
from karrio.providers.canadapost.pickup.create import (
|
14
|
+
parse_pickup_response,
|
15
|
+
_create_pickup_request,
|
16
|
+
_get_pickup,
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
def parse_pickup_update_response(
|
21
|
+
_response: lib.Deserializable[Element], settings: Settings
|
22
|
+
) -> Tuple[PickupDetails, List[Message]]:
|
23
|
+
response = _response.deserialize()
|
24
|
+
return parse_pickup_response(response, settings)
|
25
|
+
|
26
|
+
|
27
|
+
def pickup_update_request(
|
28
|
+
payload: PickupUpdateRequest, settings: Settings
|
29
|
+
) -> Serializable:
|
30
|
+
request: Pipeline = Pipeline(
|
31
|
+
update_pickup=lambda *_: _update_pickup(payload, settings),
|
32
|
+
get_pickup=partial(_get_pickup, payload=payload, settings=settings),
|
33
|
+
)
|
34
|
+
return Serializable(request)
|
35
|
+
|
36
|
+
|
37
|
+
def _update_pickup(payload: PickupUpdateRequest, settings: Settings) -> Job:
|
38
|
+
data = Serializable(
|
39
|
+
dict(
|
40
|
+
confirmation_number=payload.confirmation_number,
|
41
|
+
data=_create_pickup_request(
|
42
|
+
cast(PickupRequest, payload), settings, update=True
|
43
|
+
),
|
44
|
+
),
|
45
|
+
_update_request_serializer,
|
46
|
+
)
|
47
|
+
fallback = "" if data is None else ""
|
48
|
+
|
49
|
+
return Job(id="update_pickup", data=data, fallback=fallback)
|
50
|
+
|
51
|
+
|
52
|
+
def _update_request_serializer(request: dict) -> dict:
|
53
|
+
pickuprequest = request["confirmation_number"]
|
54
|
+
data = request["data"].serialize()
|
55
|
+
return dict(pickuprequest=pickuprequest, data=data)
|
@@ -0,0 +1,192 @@
|
|
1
|
+
import karrio.schemas.canadapost.rating as canadapost
|
2
|
+
import typing
|
3
|
+
import karrio.lib as lib
|
4
|
+
import karrio.core.units as units
|
5
|
+
import karrio.core.errors as errors
|
6
|
+
import karrio.core.models as models
|
7
|
+
import karrio.providers.canadapost.error as provider_error
|
8
|
+
import karrio.providers.canadapost.units as provider_units
|
9
|
+
import karrio.providers.canadapost.utils as provider_utils
|
10
|
+
|
11
|
+
|
12
|
+
def parse_rate_response(
|
13
|
+
_response: lib.Deserializable[lib.Element],
|
14
|
+
settings: provider_utils.Settings,
|
15
|
+
) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
|
16
|
+
responses = _response.deserialize()
|
17
|
+
|
18
|
+
package_rates: typing.List[typing.Tuple[str, typing.List[models.RateDetails]]] = [
|
19
|
+
(
|
20
|
+
f"{_}",
|
21
|
+
[
|
22
|
+
_extract_details(node, settings)
|
23
|
+
for node in lib.find_element("price-quote", response)
|
24
|
+
],
|
25
|
+
)
|
26
|
+
for _, response in enumerate(responses, start=1)
|
27
|
+
]
|
28
|
+
|
29
|
+
messages = provider_error.parse_error_response(responses, settings)
|
30
|
+
rates = lib.to_multi_piece_rates(package_rates)
|
31
|
+
|
32
|
+
return rates, messages
|
33
|
+
|
34
|
+
|
35
|
+
def _extract_details(
|
36
|
+
node: lib.Element, settings: provider_utils.Settings
|
37
|
+
) -> models.RateDetails:
|
38
|
+
quote = lib.to_object(canadapost.price_quoteType, node)
|
39
|
+
service = provider_units.ServiceType.map(quote.service_code)
|
40
|
+
|
41
|
+
adjustments = getattr(quote.price_details.adjustments, "adjustment", [])
|
42
|
+
charges = [
|
43
|
+
("Base charge", quote.price_details.base),
|
44
|
+
("GST", quote.price_details.taxes.gst.valueOf_),
|
45
|
+
("PST", quote.price_details.taxes.pst.valueOf_),
|
46
|
+
("HST", quote.price_details.taxes.hst.valueOf_),
|
47
|
+
*((a.adjustment_name, a.adjustment_cost) for a in adjustments),
|
48
|
+
]
|
49
|
+
|
50
|
+
return models.RateDetails(
|
51
|
+
carrier_name=settings.carrier_name,
|
52
|
+
carrier_id=settings.carrier_id,
|
53
|
+
currency=units.Currency.CAD.name,
|
54
|
+
transit_days=quote.service_standard.expected_transit_time,
|
55
|
+
service=service.name_or_key,
|
56
|
+
total_charge=lib.to_money(quote.price_details.due or 0),
|
57
|
+
extra_charges=[
|
58
|
+
models.ChargeDetails(
|
59
|
+
name=name,
|
60
|
+
currency=units.Currency.CAD.name,
|
61
|
+
amount=lib.to_money(amount),
|
62
|
+
)
|
63
|
+
for name, amount in charges
|
64
|
+
if amount
|
65
|
+
],
|
66
|
+
meta=dict(service_name=(service.name or quote.service_name)),
|
67
|
+
)
|
68
|
+
|
69
|
+
|
70
|
+
def rate_request(
|
71
|
+
payload: models.RateRequest,
|
72
|
+
settings: provider_utils.Settings,
|
73
|
+
) -> lib.Serializable:
|
74
|
+
"""Create the appropriate Canada Post rate request depending on the destination
|
75
|
+
|
76
|
+
:param settings: Karrio carrier connection settings
|
77
|
+
:param payload: Karrio unified API rate request data
|
78
|
+
:return: a domestic or international Canada post compatible request
|
79
|
+
:raises: an OriginNotServicedError when origin country is not serviced by the carrier
|
80
|
+
"""
|
81
|
+
if (
|
82
|
+
payload.shipper.country_code
|
83
|
+
and payload.shipper.country_code != units.Country.CA.name
|
84
|
+
):
|
85
|
+
raise errors.OriginNotServicedError(payload.shipper.country_code)
|
86
|
+
|
87
|
+
services = lib.to_services(payload.services, provider_units.ServiceType)
|
88
|
+
options = lib.to_shipping_options(
|
89
|
+
payload.options,
|
90
|
+
initializer=provider_units.shipping_options_initializer,
|
91
|
+
)
|
92
|
+
packages = lib.to_packages(
|
93
|
+
payload.parcels,
|
94
|
+
provider_units.PackagePresets,
|
95
|
+
required=["weight"],
|
96
|
+
options=options,
|
97
|
+
package_option_type=provider_units.ShippingOption,
|
98
|
+
shipping_options_initializer=provider_units.shipping_options_initializer,
|
99
|
+
)
|
100
|
+
|
101
|
+
requests = [
|
102
|
+
canadapost.mailing_scenario(
|
103
|
+
customer_number=settings.customer_number,
|
104
|
+
contract_id=settings.contract_id,
|
105
|
+
promo_code=None,
|
106
|
+
quote_type=None,
|
107
|
+
expected_mailing_date=package.options.shipment_date.state,
|
108
|
+
options=(
|
109
|
+
canadapost.optionsType(
|
110
|
+
option=[
|
111
|
+
canadapost.optionType(
|
112
|
+
option_code=option.code,
|
113
|
+
option_amount=lib.to_money(option.state),
|
114
|
+
)
|
115
|
+
for _, option in package.options.items()
|
116
|
+
if option.state is not False
|
117
|
+
]
|
118
|
+
)
|
119
|
+
if any(
|
120
|
+
[
|
121
|
+
option
|
122
|
+
for _, option in package.options.items()
|
123
|
+
if option.state is not False
|
124
|
+
]
|
125
|
+
)
|
126
|
+
else None
|
127
|
+
),
|
128
|
+
parcel_characteristics=canadapost.parcel_characteristicsType(
|
129
|
+
weight=package.weight.map(provider_units.MeasurementOptions).KG,
|
130
|
+
dimensions=canadapost.dimensionsType(
|
131
|
+
length=package.length.map(provider_units.MeasurementOptions).CM,
|
132
|
+
width=package.width.map(provider_units.MeasurementOptions).CM,
|
133
|
+
height=package.height.map(provider_units.MeasurementOptions).CM,
|
134
|
+
),
|
135
|
+
unpackaged=None,
|
136
|
+
mailing_tube=None,
|
137
|
+
oversized=None,
|
138
|
+
),
|
139
|
+
services=(
|
140
|
+
canadapost.servicesType(service_code=[svc.value for svc in services])
|
141
|
+
if any(services)
|
142
|
+
else None
|
143
|
+
),
|
144
|
+
origin_postal_code=provider_utils.format_ca_postal_code(
|
145
|
+
payload.shipper.postal_code
|
146
|
+
),
|
147
|
+
destination=canadapost.destinationType(
|
148
|
+
domestic=(
|
149
|
+
canadapost.domesticType(
|
150
|
+
postal_code=provider_utils.format_ca_postal_code(
|
151
|
+
payload.recipient.postal_code
|
152
|
+
)
|
153
|
+
)
|
154
|
+
if (payload.recipient.country_code == units.Country.CA.name)
|
155
|
+
else None
|
156
|
+
),
|
157
|
+
united_states=(
|
158
|
+
canadapost.united_statesType(
|
159
|
+
zip_code=provider_utils.format_ca_postal_code(
|
160
|
+
payload.recipient.postal_code
|
161
|
+
)
|
162
|
+
)
|
163
|
+
if (payload.recipient.country_code == units.Country.US.name)
|
164
|
+
else None
|
165
|
+
),
|
166
|
+
international=(
|
167
|
+
canadapost.internationalType(
|
168
|
+
country_code=provider_utils.format_ca_postal_code(
|
169
|
+
payload.recipient.postal_code
|
170
|
+
)
|
171
|
+
)
|
172
|
+
if (
|
173
|
+
payload.recipient.country_code
|
174
|
+
not in [units.Country.US.name, units.Country.CA.name]
|
175
|
+
)
|
176
|
+
else None
|
177
|
+
),
|
178
|
+
),
|
179
|
+
)
|
180
|
+
for package in packages
|
181
|
+
]
|
182
|
+
|
183
|
+
return lib.Serializable(
|
184
|
+
requests,
|
185
|
+
lambda __: [
|
186
|
+
lib.to_xml(
|
187
|
+
request,
|
188
|
+
namespacedef_='xmlns="http://www.canadapost.ca/ws/ship/rate-v4"',
|
189
|
+
)
|
190
|
+
for request in __
|
191
|
+
],
|
192
|
+
)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
import karrio.schemas.canadapost.shipment as canadapost
|
2
|
+
import typing
|
3
|
+
import karrio.lib as lib
|
4
|
+
import karrio.core.units as units
|
5
|
+
import karrio.core.errors as errors
|
6
|
+
import karrio.core.models as models
|
7
|
+
import karrio.providers.canadapost.error as provider_error
|
8
|
+
import karrio.providers.canadapost.units as provider_units
|
9
|
+
import karrio.providers.canadapost.utils as provider_utils
|
10
|
+
|
11
|
+
|
12
|
+
def parse_shipment_cancel_response(
|
13
|
+
_responses: lib.Deserializable[typing.List[typing.Tuple[str, lib.Element]]],
|
14
|
+
settings: provider_utils.Settings,
|
15
|
+
) -> typing.Tuple[models.ConfirmationDetails, typing.List[models.Message]]:
|
16
|
+
responses = [
|
17
|
+
(_, provider_error.parse_error_response(response, settings, shipment_id=_))
|
18
|
+
for _, response in _responses.deserialize()
|
19
|
+
]
|
20
|
+
messages: typing.List[models.Message] = sum([__ for _, __ in responses], start=[])
|
21
|
+
success = any([len(errors) == 0 for _, errors in responses])
|
22
|
+
|
23
|
+
confirmation: models.ConfirmationDetails = (
|
24
|
+
models.ConfirmationDetails(
|
25
|
+
carrier_id=settings.carrier_id,
|
26
|
+
carrier_name=settings.carrier_name,
|
27
|
+
success=success,
|
28
|
+
operation="Cancel Shipment",
|
29
|
+
)
|
30
|
+
if success
|
31
|
+
else None
|
32
|
+
)
|
33
|
+
|
34
|
+
return confirmation, messages
|
35
|
+
|
36
|
+
|
37
|
+
def shipment_cancel_request(
|
38
|
+
payload: models.ShipmentCancelRequest, _
|
39
|
+
) -> lib.Serializable:
|
40
|
+
request = list(
|
41
|
+
set(
|
42
|
+
[
|
43
|
+
payload.shipment_identifier,
|
44
|
+
*((payload.options or {}).get("shipment_identifiers") or []),
|
45
|
+
]
|
46
|
+
)
|
47
|
+
)
|
48
|
+
|
49
|
+
return lib.Serializable(
|
50
|
+
request,
|
51
|
+
lib.identity,
|
52
|
+
dict(email=payload.options.get("email")),
|
53
|
+
)
|