karrio-freightcom 2025.5.6__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.
@@ -0,0 +1,3 @@
1
+ from karrio.mappers.freightcom.mapper import Mapper
2
+ from karrio.mappers.freightcom.proxy import Proxy
3
+ from karrio.mappers.freightcom.settings import Settings
@@ -0,0 +1,55 @@
1
+ from typing import List, Tuple
2
+ from karrio.api.mapper import Mapper as BaseMapper
3
+ from karrio.mappers.freightcom.settings import Settings
4
+ from karrio.core.utils.serializable import Deserializable, Serializable
5
+ from karrio.core.models import (
6
+ RateRequest,
7
+ ShipmentRequest,
8
+ ShipmentDetails,
9
+ RateDetails,
10
+ Message,
11
+ ShipmentCancelRequest,
12
+ ConfirmationDetails,
13
+ )
14
+ from karrio.providers.freightcom import (
15
+ parse_quote_reply,
16
+ quote_request,
17
+ parse_shipping_reply,
18
+ shipping_request,
19
+ shipment_cancel_request,
20
+ parse_shipment_cancel_reply,
21
+ )
22
+
23
+
24
+ class Mapper(BaseMapper):
25
+ settings: Settings
26
+
27
+ # Request Mappers
28
+
29
+ def create_rate_request(self, payload: RateRequest) -> Serializable:
30
+ return quote_request(payload, self.settings)
31
+
32
+ def create_shipment_request(self, payload: ShipmentRequest) -> Serializable:
33
+ return shipping_request(payload, self.settings)
34
+
35
+ def create_cancel_shipment_request(
36
+ self, payload: ShipmentCancelRequest
37
+ ) -> Serializable:
38
+ return shipment_cancel_request(payload, self.settings)
39
+
40
+ # Response Parsers
41
+
42
+ def parse_rate_response(
43
+ self, response: Deserializable
44
+ ) -> Tuple[List[RateDetails], List[Message]]:
45
+ return parse_quote_reply(response, self.settings)
46
+
47
+ def parse_shipment_response(
48
+ self, response: Deserializable
49
+ ) -> Tuple[ShipmentDetails, List[Message]]:
50
+ return parse_shipping_reply(response, self.settings)
51
+
52
+ def parse_cancel_shipment_response(
53
+ self, response: Deserializable
54
+ ) -> Tuple[ConfirmationDetails, List[Message]]:
55
+ return parse_shipment_cancel_reply(response, self.settings)
@@ -0,0 +1,38 @@
1
+ from karrio.core.utils import request as http, XP
2
+ from karrio.api.proxy import Proxy as BaseProxy
3
+ from karrio.mappers.freightcom.settings import Settings
4
+ from karrio.core.utils.serializable import Serializable, Deserializable
5
+
6
+
7
+ class Proxy(BaseProxy):
8
+ settings: Settings
9
+
10
+ def get_rates(self, request: Serializable) -> Deserializable:
11
+ response = http(
12
+ url=self.settings.server_url,
13
+ data=request.serialize(),
14
+ trace=self.trace_as("xml"),
15
+ method="POST",
16
+ headers={"Content-Type": "application/xml"},
17
+ )
18
+ return Deserializable(response, XP.to_xml)
19
+
20
+ def create_shipment(self, request: Serializable) -> Deserializable:
21
+ response = http(
22
+ url=self.settings.server_url,
23
+ data=request.serialize(),
24
+ trace=self.trace_as("xml"),
25
+ method="POST",
26
+ headers={"Content-Type": "application/xml"},
27
+ )
28
+ return Deserializable(response, XP.to_xml)
29
+
30
+ def cancel_shipment(self, request: Serializable) -> Deserializable:
31
+ response = http(
32
+ url=self.settings.server_url,
33
+ data=request.serialize(),
34
+ trace=self.trace_as("xml"),
35
+ method="POST",
36
+ headers={"Content-Type": "application/xml"},
37
+ )
38
+ return Deserializable(response, XP.to_xml)
@@ -0,0 +1,19 @@
1
+ """Karrio freightcom connection settings."""
2
+
3
+ import attr
4
+ from karrio.providers.freightcom.utils import Settings as BaseSettings
5
+
6
+
7
+ @attr.s(auto_attribs=True)
8
+ class Settings(BaseSettings):
9
+ """Freightcom connection settings."""
10
+
11
+ username: str
12
+ password: str
13
+
14
+ id: str = None
15
+ test_mode: bool = False
16
+ carrier_id: str = "freightcom"
17
+ account_country_code: str = None
18
+ metadata: dict = {}
19
+ config: dict = {}
@@ -0,0 +1,19 @@
1
+ import karrio.core.metadata as metadata
2
+ import karrio.mappers.freightcom as mappers
3
+ import karrio.providers.freightcom.units as units
4
+
5
+
6
+ METADATA = metadata.PluginMetadata(
7
+ status="deprecated",
8
+ id="freightcom",
9
+ label="Freightcom",
10
+ is_hub=True,
11
+ # Integrations
12
+ Mapper=mappers.Mapper,
13
+ Proxy=mappers.Proxy,
14
+ Settings=mappers.Settings,
15
+ # Data Units
16
+ options=units.ShippingOption,
17
+ services=units.ShippingService,
18
+ hub_carriers=units.CARRIER_IDS,
19
+ )
@@ -0,0 +1,6 @@
1
+ from karrio.providers.freightcom.quote import parse_quote_reply, quote_request
2
+ from karrio.providers.freightcom.shipping import (
3
+ parse_shipping_reply,
4
+ shipping_request,
5
+ )
6
+ from karrio.providers.freightcom.void_shipment import shipment_cancel_request, parse_shipment_cancel_reply
@@ -0,0 +1,40 @@
1
+ from typing import List
2
+ from karrio.schemas.freightcom.error import ErrorType
3
+ from karrio.schemas.freightcom.quote_reply import CarrierErrorMessageType
4
+ from karrio.core.models import Message
5
+ from karrio.core.utils import Element, XP
6
+ from karrio.providers.freightcom.utils import Settings
7
+
8
+
9
+ def parse_error_response(response: Element, settings: Settings) -> List[Message]:
10
+ errors = XP.find("Error", response, ErrorType)
11
+ carrier_errors = XP.find("CarrierErrorMessage", response, CarrierErrorMessageType)
12
+
13
+ return [
14
+ *[_extract_error(er, settings) for er in errors if er.Message != ""],
15
+ *[
16
+ _extract_carrier_error(er, settings)
17
+ for er in carrier_errors
18
+ if er.errorMessage0 != ""
19
+ ],
20
+ ]
21
+
22
+
23
+ def _extract_carrier_error(
24
+ error: CarrierErrorMessageType, settings: Settings
25
+ ) -> Message:
26
+ return Message(
27
+ code="CarrierErrorMessage",
28
+ carrier_name=settings.carrier_name,
29
+ carrier_id=settings.carrier_id,
30
+ message=error.errorMessage0,
31
+ )
32
+
33
+
34
+ def _extract_error(error: ErrorType, settings: Settings) -> Message:
35
+ return Message(
36
+ code="Error",
37
+ carrier_name=settings.carrier_name,
38
+ carrier_id=settings.carrier_id,
39
+ message=error.Message,
40
+ )
@@ -0,0 +1,179 @@
1
+ from karrio.schemas.freightcom.quote_request import (
2
+ Freightcom,
3
+ QuoteRequestType,
4
+ FromType,
5
+ ToType,
6
+ PackagesType,
7
+ PackageType,
8
+ )
9
+ from karrio.schemas.freightcom.quote_reply import QuoteType
10
+
11
+ import typing
12
+ import karrio.lib as lib
13
+ import karrio.core.models as models
14
+ import karrio.providers.freightcom.error as provider_error
15
+ import karrio.providers.freightcom.units as provider_units
16
+ import karrio.providers.freightcom.utils as provider_utils
17
+
18
+
19
+ def parse_quote_reply(
20
+ _response: lib.Deserializable[lib.Element], settings: provider_utils.Settings
21
+ ) -> typing.Tuple[typing.List[models.RateDetails], typing.List[models.Message]]:
22
+ response = _response.deserialize()
23
+ estimates = lib.find_element("Quote", response)
24
+
25
+ return (
26
+ [_extract_rate(node, settings) for node in estimates],
27
+ provider_error.parse_error_response(response, settings),
28
+ )
29
+
30
+
31
+ def _extract_rate(
32
+ node: lib.Element,
33
+ settings: provider_utils.Settings,
34
+ ) -> models.RateDetails:
35
+ quote = lib.to_object(QuoteType, node)
36
+ rate_provider, service, service_name = provider_units.ShippingService.info(
37
+ quote.serviceId,
38
+ quote.carrierId,
39
+ quote.serviceName,
40
+ quote.carrierName,
41
+ )
42
+ charges = [
43
+ ("Base charge", quote.baseCharge),
44
+ ("Fuel surcharge", quote.fuelSurcharge),
45
+ *((surcharge.name, surcharge.amount) for surcharge in quote.Surcharge),
46
+ ]
47
+
48
+ return models.RateDetails(
49
+ carrier_name=settings.carrier_name,
50
+ carrier_id=settings.carrier_id,
51
+ currency=quote.currency,
52
+ service=service,
53
+ total_charge=lib.to_decimal(quote.totalCharge),
54
+ transit_days=quote.transitDays,
55
+ extra_charges=[
56
+ models.ChargeDetails(
57
+ name=name,
58
+ currency="CAD",
59
+ amount=lib.to_decimal(amount),
60
+ )
61
+ for name, amount in charges
62
+ if amount
63
+ ],
64
+ meta=dict(rate_provider=rate_provider, service_name=service_name),
65
+ )
66
+
67
+
68
+ def quote_request(
69
+ payload: models.RateRequest,
70
+ settings: provider_utils.Settings,
71
+ ) -> lib.Serializable:
72
+ shipper = lib.to_address(payload.shipper)
73
+ recipient = lib.to_address(payload.recipient)
74
+ packages = lib.to_packages(
75
+ payload.parcels,
76
+ package_option_type=provider_units.ShippingOption,
77
+ required=["weight", "height", "width", "length"],
78
+ )
79
+ options = lib.to_shipping_options(
80
+ payload.options,
81
+ package_options=packages.options,
82
+ initializer=provider_units.shipping_options_initializer,
83
+ )
84
+ packaging_type = provider_units.FreightPackagingType[
85
+ packages.package_type or "small_box"
86
+ ].value
87
+ packaging = (
88
+ "Pallet"
89
+ if packaging_type in [provider_units.FreightPackagingType.pallet.value]
90
+ else "Package"
91
+ )
92
+ service = (
93
+ lib.to_services(payload.services, provider_units.ShippingService).first
94
+ or provider_units.ShippingService.freightcom_all
95
+ )
96
+
97
+ request = Freightcom(
98
+ username=settings.username,
99
+ password=settings.password,
100
+ version="3.1.0",
101
+ QuoteRequest=QuoteRequestType(
102
+ saturdayPickupRequired=options.freightcom_saturday_pickup_required.state,
103
+ homelandSecurity=options.freightcom_homeland_security.state,
104
+ pierCharge=None,
105
+ exhibitionConventionSite=options.freightcom_exhibition_convention_site.state,
106
+ militaryBaseDelivery=options.freightcom_military_base_delivery.state,
107
+ customsIn_bondFreight=options.freightcom_customs_in_bond_freight.state,
108
+ limitedAccess=options.freightcom_limited_access.state,
109
+ excessLength=options.freightcom_excess_length.state,
110
+ tailgatePickup=options.freightcom_tailgate_pickup.state,
111
+ residentialPickup=options.freightcom_residential_pickup.state,
112
+ crossBorderFee=None,
113
+ notifyRecipient=options.freightcom_notify_recipient.state,
114
+ singleShipment=options.freightcom_single_shipment.state,
115
+ tailgateDelivery=options.freightcom_tailgate_delivery.state,
116
+ residentialDelivery=options.freightcom_residential_delivery.state,
117
+ insuranceType=options.insurance.state is not None,
118
+ scheduledShipDate=None,
119
+ insideDelivery=options.freightcom_inside_delivery.state,
120
+ isSaturdayService=options.freightcom_is_saturday_service.state,
121
+ dangerousGoodsType=options.freightcom_dangerous_goods_type.state,
122
+ serviceId=service.value,
123
+ stackable=options.freightcom_stackable.state,
124
+ From=FromType(
125
+ id=None,
126
+ company=shipper.company_name or " ",
127
+ instructions=None,
128
+ email=shipper.email,
129
+ attention=shipper.person_name,
130
+ phone=shipper.phone_number,
131
+ tailgateRequired=None,
132
+ residential=shipper.residential,
133
+ address1=shipper.street,
134
+ address2=lib.text(shipper.address_line2),
135
+ city=shipper.city,
136
+ state=shipper.state_code,
137
+ zip=shipper.postal_code,
138
+ country=shipper.country_code,
139
+ ),
140
+ To=ToType(
141
+ id=None,
142
+ company=recipient.company_name or " ",
143
+ notifyRecipient=None,
144
+ instructions=None,
145
+ email=recipient.email,
146
+ attention=recipient.person_name,
147
+ phone=recipient.phone_number,
148
+ tailgateRequired=None,
149
+ residential=recipient.residential,
150
+ address1=recipient.street,
151
+ address2=lib.text(recipient.address_line2),
152
+ city=recipient.city,
153
+ state=recipient.state_code,
154
+ zip=recipient.postal_code,
155
+ country=recipient.country_code,
156
+ ),
157
+ COD=None,
158
+ Packages=PackagesType(
159
+ Package=[
160
+ PackageType(
161
+ length=provider_utils.ceil(package.length.IN),
162
+ width=provider_utils.ceil(package.width.IN),
163
+ height=provider_utils.ceil(package.height.IN),
164
+ weight=provider_utils.ceil(package.weight.LB),
165
+ type_=packaging_type,
166
+ freightClass=package.parcel.freight_class,
167
+ nmfcCode=None,
168
+ insuranceAmount=package.options.insurance.state,
169
+ codAmount=package.options.cash_on_delivery.state,
170
+ description=package.parcel.description,
171
+ )
172
+ for package in packages
173
+ ],
174
+ type_=packaging,
175
+ ),
176
+ ),
177
+ )
178
+
179
+ return lib.Serializable(request, provider_utils.standard_request_serializer)
@@ -0,0 +1,294 @@
1
+ from karrio.schemas.freightcom.shipping_request import (
2
+ Freightcom,
3
+ ShippingRequestType,
4
+ FromType,
5
+ ToType,
6
+ PackagesType,
7
+ PackageType,
8
+ PaymentType as RequestPaymentType,
9
+ CODType,
10
+ CODReturnAddressType,
11
+ ContactType,
12
+ ReferenceType,
13
+ CustomsInvoiceType,
14
+ ItemType,
15
+ BillToType,
16
+ )
17
+ from karrio.schemas.freightcom.shipping_reply import (
18
+ ShippingReplyType,
19
+ QuoteType,
20
+ )
21
+
22
+ import typing
23
+ import karrio.lib as lib
24
+ import karrio.core.models as models
25
+ import karrio.providers.freightcom.error as provider_error
26
+ import karrio.providers.freightcom.units as provider_units
27
+ import karrio.providers.freightcom.utils as provider_utils
28
+
29
+
30
+ def parse_shipping_reply(
31
+ _response: lib.Deserializable[lib.Element],
32
+ settings: provider_utils.Settings,
33
+ ) -> typing.Tuple[models.ShipmentDetails, typing.List[models.Message]]:
34
+ response = _response.deserialize()
35
+ shipping_node = lib.find_element("ShippingReply", response, first=True)
36
+ shipment = (
37
+ _extract_shipment(shipping_node, settings)
38
+ if shipping_node is not None
39
+ else None
40
+ )
41
+
42
+ return shipment, provider_error.parse_error_response(response, settings)
43
+
44
+
45
+ def _extract_shipment(
46
+ node: lib.Element,
47
+ settings: provider_utils.Settings,
48
+ ) -> models.ShipmentDetails:
49
+ shipping = lib.to_object(ShippingReplyType, node)
50
+ quote: QuoteType = shipping.Quote or QuoteType()
51
+
52
+ tracking_number = getattr(
53
+ next(iter(shipping.Package), None), "trackingNumber", None
54
+ )
55
+ rate_provider, service, service_name = provider_units.ShippingService.info(
56
+ quote.serviceId, quote.carrierId, quote.serviceName, quote.carrierName
57
+ )
58
+ invoice = dict(invoice=shipping.CustomsInvoice) if shipping.CustomsInvoice else {}
59
+ charges = [
60
+ ("Base charge", quote.baseCharge),
61
+ ("Fuel surcharge", quote.fuelSurcharge),
62
+ *((surcharge.name, surcharge.amount) for surcharge in quote.Surcharge),
63
+ ]
64
+
65
+ return models.ShipmentDetails(
66
+ carrier_name=settings.carrier_name,
67
+ carrier_id=settings.carrier_id,
68
+ tracking_number=tracking_number,
69
+ shipment_identifier=shipping.Order.id,
70
+ selected_rate=(
71
+ models.RateDetails(
72
+ carrier_name=settings.carrier_name,
73
+ carrier_id=settings.carrier_id,
74
+ service=service,
75
+ currency=quote.currency,
76
+ total_charge=lib.to_decimal(quote.totalCharge),
77
+ transit_days=quote.transitDays,
78
+ extra_charges=[
79
+ models.ChargeDetails(
80
+ name=name,
81
+ currency="CAD",
82
+ amount=lib.to_decimal(amount),
83
+ )
84
+ for name, amount in charges
85
+ if amount
86
+ ],
87
+ meta=dict(rate_provider=rate_provider, service_name=service_name),
88
+ )
89
+ if shipping.Quote is not None
90
+ else None
91
+ ),
92
+ docs=models.Documents(label=shipping.Labels, **invoice),
93
+ meta=dict(
94
+ rate_provider=rate_provider,
95
+ service_name=service_name,
96
+ tracking_url=shipping.TrackingURL,
97
+ ),
98
+ )
99
+
100
+
101
+ def shipping_request(
102
+ payload: models.ShipmentRequest,
103
+ settings: provider_utils.Settings,
104
+ ) -> lib.Serializable:
105
+ shipper = lib.to_address(payload.shipper)
106
+ recipient = lib.to_address(payload.recipient)
107
+ service = provider_units.ShippingService.map(payload.service).value_or_key
108
+ packages = lib.to_packages(
109
+ payload.parcels,
110
+ package_option_type=provider_units.ShippingOption,
111
+ required=["weight", "height", "width", "length"],
112
+ )
113
+ options = lib.to_shipping_options(
114
+ payload.options,
115
+ package_options=packages.options,
116
+ initializer=provider_units.shipping_options_initializer,
117
+ )
118
+
119
+ is_intl = shipper.country_code != recipient.country_code
120
+ customs = lib.to_customs_info(
121
+ payload.customs,
122
+ shipper=payload.shipper,
123
+ recipient=payload.recipient,
124
+ weight_unit=packages.weight_unit,
125
+ default_to=(
126
+ models.Customs(
127
+ commodities=(
128
+ packages.items
129
+ if any(packages.items)
130
+ else [
131
+ models.Commodity(
132
+ quantity=1,
133
+ sku=f"000{index}",
134
+ weight=pkg.weight.value,
135
+ weight_unit=pkg.weight_unit.value,
136
+ description=pkg.parcel.content,
137
+ )
138
+ for index, pkg in enumerate(packages, start=1)
139
+ ]
140
+ )
141
+ )
142
+ if is_intl
143
+ else None
144
+ ),
145
+ )
146
+
147
+ packaging_type = provider_units.FreightPackagingType.map(
148
+ packages.package_type or "small_box"
149
+ ).value
150
+ packaging = (
151
+ "Pallet"
152
+ if packaging_type in [provider_units.FreightPackagingType.pallet.value]
153
+ else "Package"
154
+ )
155
+ payment = payload.payment or models.Payment()
156
+ payment_type = (
157
+ provider_units.PaymentType[payment.paid_by] if payload.payment else None
158
+ )
159
+
160
+ request = Freightcom(
161
+ version="3.1.0",
162
+ username=settings.username,
163
+ password=settings.password,
164
+ ShippingRequest=ShippingRequestType(
165
+ saturdayPickupRequired=options.freightcom_saturday_pickup_required.state,
166
+ homelandSecurity=options.freightcom_homeland_security.state,
167
+ pierCharge=None,
168
+ exhibitionConventionSite=options.freightcom_exhibition_convention_site.state,
169
+ militaryBaseDelivery=options.freightcom_military_base_delivery.state,
170
+ customsIn_bondFreight=options.freightcom_customs_in_bond_freight.state,
171
+ limitedAccess=options.freightcom_limited_access.state,
172
+ excessLength=options.freightcom_excess_length.state,
173
+ tailgatePickup=options.freightcom_tailgate_pickup.state,
174
+ residentialPickup=options.freightcom_residential_pickup.state,
175
+ crossBorderFee=None,
176
+ notifyRecipient=options.freightcom_notify_recipient.state,
177
+ singleShipment=options.freightcom_single_shipment.state,
178
+ tailgateDelivery=options.freightcom_tailgate_delivery.state,
179
+ residentialDelivery=options.freightcom_residential_delivery.state,
180
+ insuranceType=(options.insurance.state is not None),
181
+ scheduledShipDate=None,
182
+ insideDelivery=options.freightcom_inside_delivery.state,
183
+ isSaturdayService=options.freightcom_is_saturday_service.state,
184
+ dangerousGoodsType=options.freightcom_dangerous_goods_type.state,
185
+ serviceId=service,
186
+ stackable=options.freightcom_stackable.state,
187
+ From=FromType(
188
+ id=None,
189
+ company=shipper.company_name,
190
+ instructions=None,
191
+ email=shipper.email,
192
+ attention=shipper.person_name,
193
+ phone=shipper.phone_number,
194
+ tailgateRequired=None,
195
+ residential=shipper.residential,
196
+ address1=shipper.street,
197
+ address2=lib.text(shipper.address_line2),
198
+ city=shipper.city,
199
+ state=shipper.state_code,
200
+ zip=shipper.postal_code,
201
+ country=shipper.country_code,
202
+ ),
203
+ To=ToType(
204
+ id=None,
205
+ company=recipient.company_name,
206
+ notifyRecipient=None,
207
+ instructions=None,
208
+ email=recipient.email,
209
+ attention=recipient.person_name,
210
+ phone=recipient.phone_number,
211
+ tailgateRequired=None,
212
+ residential=recipient.residential,
213
+ address1=recipient.street,
214
+ address2=lib.text(recipient.address_line2),
215
+ city=recipient.city,
216
+ state=recipient.state_code,
217
+ zip=recipient.postal_code,
218
+ country=recipient.country_code,
219
+ ),
220
+ COD=(
221
+ CODType(
222
+ paymentType=provider_units.PaymentType.recipient.value,
223
+ CODReturnAddress=CODReturnAddressType(
224
+ codCompany=recipient.company_name,
225
+ codName=recipient.person_name,
226
+ codAddress1=lib.text(recipient.address_line1),
227
+ codCity=recipient.city,
228
+ codStateCode=recipient.state_code,
229
+ codZip=recipient.postal_code,
230
+ codCountry=recipient.country_code,
231
+ ),
232
+ )
233
+ if options.cash_on_delivery.state is not None
234
+ else None
235
+ ),
236
+ Packages=PackagesType(
237
+ Package=[
238
+ PackageType(
239
+ length=provider_utils.ceil(package.length.IN),
240
+ width=provider_utils.ceil(package.width.IN),
241
+ height=provider_utils.ceil(package.height.IN),
242
+ weight=provider_utils.ceil(package.weight.LB),
243
+ type_=packaging_type,
244
+ freightClass=package.parcel.freight_class,
245
+ nmfcCode=None,
246
+ insuranceAmount=package.options.insurance.state,
247
+ codAmount=package.options.cash_on_delivery.state,
248
+ description=package.parcel.description,
249
+ )
250
+ for package in packages
251
+ ],
252
+ type_=packaging,
253
+ ),
254
+ Payment=(RequestPaymentType(type_=payment_type) if payment_type else None),
255
+ Reference=(
256
+ [ReferenceType(name="REF", code=payload.reference)]
257
+ if payload.reference != ""
258
+ else None
259
+ ),
260
+ CustomsInvoice=(
261
+ CustomsInvoiceType(
262
+ BillTo=BillToType(
263
+ company=customs.duty_billing_address.company_name,
264
+ name=customs.duty_billing_address.person_name,
265
+ address1=customs.duty_billing_address.address_line,
266
+ city=customs.duty_billing_address.city,
267
+ state=customs.duty_billing_address.state_code,
268
+ zip=customs.duty_billing_address.postal_code,
269
+ country=customs.duty_billing_address.country_code,
270
+ ),
271
+ Contact=ContactType(
272
+ name=customs.duty_billing_address.person_name,
273
+ phone=customs.duty_billing_address.phone_number,
274
+ ),
275
+ Item=[
276
+ ItemType(
277
+ code=(item.hs_code or item.sku or "0000"),
278
+ description=lib.text(
279
+ item.description or item.title or "item"
280
+ ),
281
+ originCountry=(item.origin_country or shipper.country_code),
282
+ unitPrice=item.value_amount,
283
+ quantity=(item.quantity or 1),
284
+ )
285
+ for item in customs.commodities
286
+ ],
287
+ )
288
+ if payload.customs
289
+ else None
290
+ ),
291
+ ),
292
+ )
293
+
294
+ return lib.Serializable(request, provider_utils.standard_request_serializer)