Skip to content

Commit 6f3088b

Browse files
committed
feat: add support for DDP shipments and customs payment method handling
- Introduce logic for handling Delivered Duty Paid (DDP) shipments, including determining appropriate payment methods. - Add support for selecting customs and duties payment method, with built-in caching for efficiency. - Ensure accurate mapping of tax recipients and guarantee customs charges for DDP shipments.
1 parent 0bce207 commit 6f3088b

3 files changed

Lines changed: 84 additions & 12 deletions

File tree

plugins/freightcom_rest/karrio/providers/freightcom_rest/rate.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,18 @@ def rate_request(
124124
# Create the carrier-specific request object
125125
is_intl = shipper.country_code != recipient.country_code
126126
is_ca_to_us = shipper.country_code == "CA" and recipient.country_code == "US"
127+
127128
customs = lib.to_customs_info(
128129
payload.customs,
129130
shipper=payload.shipper,
130131
recipient=payload.recipient,
131132
weight_unit=packages.weight_unit,
132133
)
134+
commodities = lib.identity(
135+
(customs.commodities if any(customs.commodities) else packages.items)
136+
if any(packages.items) or any(customs.commodities)
137+
else []
138+
)
133139

134140
packaging_type = provider_units.PackagingType.map(packages.package_type or "small_box").value
135141
ship_datetime = lib.to_next_business_datetime(
@@ -262,19 +268,19 @@ def rate_request(
262268
products=[
263269
freightcom_rest_req.ProductType(
264270
hs_code=item.hs_code,
265-
country_of_origin=item.origin_country,
266-
num_units=item.quantity,
271+
country_of_origin=item.origin_country or shipper.country_code,
272+
num_units=item.quantity or 1,
267273
unit_price=freightcom_rest_req.TotalCostType(
268-
currency=item.value_currency,
269-
value=str(int(item.value_amount * 100))
274+
currency=item.value_currency or "CAD",
275+
value=str(int((item.value_amount or 0) * 100))
270276
),
271-
description=item.description,
277+
description=item.description or item.title,
272278
fda_regulated="no"
273-
) for item in customs.commodities
274-
] if customs and customs.commodities else [],
275-
request_guaranteed_customs_charges=options.request_guaranteed_customs_charges.state if hasattr(options, 'request_guaranteed_customs_charges') else None
279+
) for item in (list(commodities) if is_ca_to_us else [])
280+
],
281+
request_guaranteed_customs_charges=settings.connection_config.request_guaranteed_customs_charges.state or True
276282
)
277-
if is_ca_to_us and customs and any(customs.commodities)
283+
if is_ca_to_us and len(commodities) > 0
278284
else None
279285
),
280286
),

plugins/freightcom_rest/karrio/providers/freightcom_rest/shipment/create.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,23 @@ def shipment_request(
213213
if not payment_method_id:
214214
raise Exception("No payment method found need to be set in config")
215215

216+
# Check if it's DDP (Delivered Duty Paid)
217+
is_ddp = (
218+
customs and (
219+
customs.incoterm == "DDP" or
220+
(customs.duty and customs.duty.paid_by == "sender")
221+
)
222+
) if customs else False
223+
224+
# For DDP shipments with Net Terms, need credit card for customs/duties payment
225+
customs_and_duties_payment_method_id = None
226+
if is_ddp and settings.connection_config.payment_method_type.state == provider_utils.PaymentMethodType.net_terms.value:
227+
customs_and_duties_payment_method_id = settings.customs_and_duties_payment_method
228+
216229
request = freightcom_rest_req.ShipmentRequestType(
217230
unique_id=str(uuid.uuid4()),
218231
payment_method_id=payment_method_id,
232+
customs_and_duties_payment_method_id=customs_and_duties_payment_method_id,
219233
service_id=provider_units.ShippingService.map(payload.service).value_or_key,
220234
details=freightcom_rest_req.ShipmentRequestDetailsType(
221235
origin=freightcom_rest_req.DestinationType(
@@ -377,11 +391,14 @@ def shipment_request(
377391
) for item in customs.commodities
378392
],
379393
tax_recipient=freightcom_rest_req.TaxRecipientType(
380-
# they require it to be receiver, has to be enabled in the api
381-
type=provider_units.PaymentType.map(
394+
# For DDP shipments, tax recipient must be 'shipper'
395+
type="shipper" if is_ddp else (
396+
provider_units.PaymentType.map(
382397
customs.duty.paid_by
383398
).value
384-
or "receiver",
399+
if customs.duty and customs.duty.paid_by
400+
else "receiver"
401+
),
385402
name=customs.duty_billing_address.company_name or customs.duty.person_name,
386403
address=freightcom_rest_req.AddressType(
387404
address_line_1=customs.duty_billing_address.address_line1,

plugins/freightcom_rest/karrio/providers/freightcom_rest/utils.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,24 @@ def payment_method(self):
5858

5959
return new_auth.get("id")
6060

61+
@property
62+
def customs_and_duties_payment_method(self):
63+
64+
if not self.connection_config.customs_and_duties_payment_method.state:
65+
raise Exception(f"Customs and duties payment method type not set")
66+
cache_key = f"payment|{self.carrier_name}|{self.connection_config.customs_and_duties_payment_method.state}|{self.api_key}"
67+
68+
payment = self.connection_cache.get(cache_key) or {}
69+
payment_id = payment.get("id")
70+
71+
if payment_id:
72+
return payment_id
73+
74+
self.connection_cache.set(cache_key, lambda: get_customs_payment_id(self))
75+
new_auth = self.connection_cache.get(cache_key)
76+
77+
return new_auth.get("id")
78+
6179

6280
def download_document_to_base64(file_url: str) -> str:
6381
return lib.request(
@@ -95,13 +113,44 @@ def get_payment_id(settings: Settings) -> dict:
95113
except Exception as e:
96114
raise
97115

116+
def get_customs_payment_id(settings: Settings) -> dict:
117+
118+
try:
119+
from karrio.mappers.freightcom_rest.proxy import Proxy
120+
121+
proxy = Proxy(settings)
122+
response = proxy._get_payments_methods()
123+
methods = response.deserialize()
124+
125+
selected_method = next((
126+
method for method in methods
127+
if settings.connection_config.customs_and_duties_payment_method.type.map(
128+
method.get('type')).name == settings.connection_config.customs_and_duties_payment_method.state
129+
), None)
130+
131+
132+
if not selected_method:
133+
raise Exception(f"Customs payment method {settings.connection_config.customs_and_duties_payment_method.state} not found in API")
134+
135+
return selected_method
136+
137+
except Exception as e:
138+
raise
139+
98140

99141
class PaymentMethodType(lib.StrEnum):
100142
net_terms = "net-terms"
101143
credit_card = "credit-card"
102144

145+
class CustomsPaymentMethodType(lib.StrEnum):
146+
"""Payment method type for customs and duties (credit-card only)"""
147+
credit_card = "credit-card"
148+
103149
class ConnectionConfig(lib.Enum):
104150
"""Carrier specific connection configs"""
105151
payment_method_type = lib.OptionEnum("payment_method_type", PaymentMethodType)
152+
customs_and_duties_payment_method = lib.OptionEnum("customs_and_duties_payment_method", CustomsPaymentMethodType)
153+
request_guaranteed_customs_charges = lib.OptionEnum("request_guaranteed_customs_charges", bool, default=True)
106154
shipping_options = lib.OptionEnum("shipping_options", list)
107155
shipping_services = lib.OptionEnum("shipping_services", list)
156+

0 commit comments

Comments
 (0)