Skip to content

Commit 9411a40

Browse files
committed
feat: implement USMCA customs support for Freightcom API v2
- Add USMCA detection for US, CA, MX routes (bidirectional) - Add use_usmca option to control USMCA handling (defaults to true) - Set cusma_included=True for USMCA-eligible shipments - Add usmca_number option for certification number - Implement PDF document upload via paperless_customs_documents - Fix customs_and_duties_payment_method_id for all DDP shipments - Support non_auto_parts option for products
1 parent 6c9736f commit 9411a40

2 files changed

Lines changed: 50 additions & 15 deletions

File tree

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

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@
2424
import uuid
2525

2626

27+
def is_usmca_eligible(shipper_country: str, recipient_country: str) -> bool:
28+
"""Check if shipment is eligible for USMCA customs handling (US, CA, MX)."""
29+
USMCA_COUNTRIES = {"US", "CA", "MX"}
30+
return (
31+
(shipper_country in USMCA_COUNTRIES and recipient_country in USMCA_COUNTRIES) and
32+
shipper_country != recipient_country
33+
)
34+
35+
2736
def parse_shipment_response(
2837
_response: lib.Deserializable[dict],
2938
settings: provider_utils.Settings,
@@ -180,7 +189,9 @@ def shipment_request(
180189
)
181190

182191
is_intl = shipper.country_code != recipient.country_code
183-
is_ca_to_us = shipper.country_code == "CA" and recipient.country_code == "US"
192+
is_usmca_route = is_usmca_eligible(shipper.country_code, recipient.country_code)
193+
use_usmca_option = options.freightcom_use_usmca.state if hasattr(options, 'freightcom_use_usmca') and options.freightcom_use_usmca.state is not None else True
194+
is_usmca = is_usmca_route and use_usmca_option
184195
customs = lib.to_customs_info(
185196
payload.customs,
186197
shipper=payload.shipper,
@@ -208,22 +219,22 @@ def shipment_request(
208219
),
209220
)
210221

211-
payment_method_id = settings.payment_method
212-
213-
if not payment_method_id:
214-
raise Exception("No payment method found need to be set in config")
215-
216-
# Check if it's DDP (Delivered Duty Paid)
217222
is_ddp = (
218223
customs and (
219224
customs.incoterm == "DDP" or
220225
(customs.duty and customs.duty.paid_by == "sender")
221226
)
222227
) if customs else False
223228

224-
# For DDP shipments with Net Terms, need credit card for customs/duties payment
229+
payment_method_id = settings.payment_method
230+
231+
if not payment_method_id:
232+
raise Exception("No payment method found need to be set in config")
233+
225234
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:
235+
if settings.connection_config.payment_method_type.state == provider_utils.PaymentMethodType.credit_card.value:
236+
customs_and_duties_payment_method_id = payment_method_id
237+
elif settings.connection_config.customs_and_duties_payment_method.state:
227238
customs_and_duties_payment_method_id = settings.customs_and_duties_payment_method
228239

229240
request = freightcom_rest_req.ShipmentRequestType(
@@ -355,20 +366,23 @@ def shipment_request(
355366
value=str(int(item.value_amount * 100))
356367
),
357368
description=item.description,
358-
fda_regulated="no"
369+
fda_regulated="no",
370+
cusma_included=True if is_usmca else None,
371+
non_auto_parts=options.freightcom_non_auto_parts.state if hasattr(options, 'freightcom_non_auto_parts') and options.freightcom_non_auto_parts.state else None,
359372
) for item in customs.commodities
360373
] if customs and customs.commodities else [],
361-
request_guaranteed_customs_charges=options.request_guaranteed_customs_charges.state if hasattr(options, 'request_guaranteed_customs_charges') else None
374+
request_guaranteed_customs_charges=settings.connection_config.request_guaranteed_customs_charges.state
362375
)
363-
if is_ca_to_us and customs and any(customs.commodities)
376+
if is_usmca and customs and any(customs.commodities)
364377
else None
365378
),
366379
),
367380
customs_invoice=(
368381
freightcom_rest_req.CustomsInvoiceType(
369382
source="details",
370383
broker=freightcom_rest_req.BrokerType(
371-
use_carrier=True,
384+
use_carrier=True,
385+
usmca_number=options.freightcom_usmca_number.state if hasattr(options, 'freightcom_usmca_number') and options.freightcom_usmca_number.state else None,
372386
),
373387
details=freightcom_rest_req.CustomsInvoiceDetailsType(
374388
products=[
@@ -387,7 +401,9 @@ def shipment_request(
387401
value=str(int(item.value_amount * 100))
388402
),
389403
description=item.description,
390-
fda_regulated="no"
404+
fda_regulated="no",
405+
cusma_included=True if is_usmca else None,
406+
non_auto_parts=options.freightcom_non_auto_parts.state if hasattr(options, 'freightcom_non_auto_parts') and options.freightcom_non_auto_parts.state else None,
391407
) for item in customs.commodities
392408
],
393409
tax_recipient=freightcom_rest_req.TaxRecipientType(
@@ -417,7 +433,23 @@ def shipment_request(
417433
)
418434
)
419435
)
420-
if customs and customs.commodities
436+
if is_intl and customs and customs.commodities
437+
else None
438+
),
439+
paperless_customs_documents=(
440+
[
441+
freightcom_rest_req.PaperlessCustomsDocumentType(
442+
type="cusma-form" if doc.get("doc_type") == "cusma-form" else (
443+
"other" if doc.get("doc_type") == "certificate_of_origin" else "other"
444+
),
445+
type_other_name=doc.get("doc_type") if doc.get("doc_type") not in ["cusma-form"] else None,
446+
file_name=doc.get("doc_name") or "document.pdf",
447+
file_base64=doc.get("doc_file"),
448+
)
449+
for doc in (options.doc_files.state or [])
450+
if doc.get("doc_type") in ["cusma-form", "certificate_of_origin"] and doc.get("doc_file")
451+
]
452+
if hasattr(options, 'doc_files') and options.doc_files.state and is_usmca
421453
else None
422454
),
423455
#TODO: validate if we need to do pickup in the ship request

plugins/freightcom_rest/karrio/providers/freightcom_rest/units.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ class ShippingOption(lib.Enum):
9292
freightcom_stackable = lib.OptionEnum("stackable", bool)
9393
freightcom_payment_method = lib.OptionEnum("payment_method", str)
9494
freightcom_request_guaranteed_customs_charges = lib.OptionEnum("request_guaranteed_customs_charges", bool)
95+
freightcom_usmca_number = lib.OptionEnum("usmca_number", str)
96+
freightcom_non_auto_parts = lib.OptionEnum("non_auto_parts", bool)
97+
freightcom_use_usmca = lib.OptionEnum("use_usmca", bool)
9598

9699
""" Unified Option type mapping """
97100
# saturday_delivery = freightcom_saturday_pickup_required

0 commit comments

Comments
 (0)