diff --git a/carrier-testing/mydhl.md b/carrier-testing/mydhl.md new file mode 100644 index 0000000000..f63603862a --- /dev/null +++ b/carrier-testing/mydhl.md @@ -0,0 +1,238 @@ +# MyDHL Express — Carrier Testing + +## Metadata + +| Field | Value | +|----------------------------|--------------------------------------| +| **Carrier Name** | `mydhl` | +| **Carrier Display Name** | MyDHL Express | +| **Environment** | Production (`express.api.dhl.com/mydhlapi`) | +| **Connection Credentials** | DE production account configured in Dashboard | +| **Last Tested** | 2026-03-04 | + +--- + +## 1. Connection Setup + +> Verify the carrier can be connected and authenticated. MyDHL uses Basic Authentication with `username`, `password`, and `account_number`. + +- [x] Carrier connection created successfully in Dashboard +- [x] Authentication succeeds (no credential errors) +- [x] Connection appears in carrier list + +**Notes:** `account_number` (DHL billing number) must be configured — without it, rate/shipment requests fail with `"required key [number] not found"`. + +--- + +## 2. Rating / Rate Fetching + +> Test rate retrieval for shipments. MyDHL provides a **live rate endpoint** via `/rates`. + +**Rate Type:** `Live API Endpoint` + +### Live API Endpoint: +- [x] Fetch domestic rates successfully +- [x] Fetch international rates successfully +- [x] Rates returned for all expected services + +**DE Domestic:** 6 services returned in EUR — EXPRESS EASY (19.29), EXPRESS DOMESTIC (27.26), EXPRESS DOMESTIC 12:00 (33.18), EXPRESS DOMESTIC 10:30 (45.00), EXPRESS DOMESTIC 9:00 (68.65), MEDICAL EXPRESS DOMESTIC (74.98). + +**DE→US International:** 5 services returned in EUR — EXPRESS EASY (112.01), EXPRESS WORLDWIDE (170.12), EXPRESS 12:00 (176.64), EXPRESS 10:30 (189.69), MEDICAL EXPRESS (206.53). + +**Notes:** Tested with temporary Bug 1 workaround applied (fix reverted after testing). Some service codes in the response don't match the `ShippingService` enum (see Bug 4). + +--- + +## 3. Shipping / Shipment Creation + +> Test creating shipments and generating labels via `/shipments`. + +**Supported:** `Yes` + +### Basic Shipment +- [x] Create domestic shipment with default service +- [ ] Label generated and downloadable as PDF (Bug 3 — label not extracted) +- [x] Tracking number returned in response +- [x] Shipment appears in shipment list + +### Service Coverage +- [ ] Create shipment with each available service (list services tested below) + +| Service Code | Service Name | Product Code | Result | +|---|---|---|---| +| mydhl_express_worldwide_b2c | EXPRESS EASY | 7 | Pass (tracking returned, no label) | +| mydhl_express_domestic | EXPRESS DOMESTIC | N | Fail — `"Requested product(s) not available at origin"` | + +### Multi-Piece Shipment +- [ ] Create shipment with 2+ parcels (Blocked — Bugs 1-2 require fixes first) + +### International Shipment (if applicable) +- [ ] Create international shipment with customs info (Blocked — Bugs 1-2 require fixes first) + +**Notes:** Tested with temporary Bug 1 + Bug 2 workarounds applied (fixes reverted after testing). Service mapping in response shows `service: "mydhl"` instead of actual service name (Bug 5). Express Domestic (N) not available for DE origin with this account — use EXPRESS EASY (7) instead. + +--- + +## 4. Label + +> Test label output options. MyDHL supports PDF, ZPL, LP2, and EPL label formats. + +**Supported:** `Yes` + +- [ ] Label downloads as PDF (Bug 3 — label content empty) +- [ ] Label format matches requested format +- [ ] Label contains correct shipper and recipient info + +**Available label formats:** `PDF`, `ZPL`, `LP2`, `EPL` + +**Notes:** DHL returns label data (7144 chars base64 PDF confirmed in raw response) but it's not extracted into the shipment response. See Bug 3. + +--- + +## 5. Shipment Cancellation + +> Test cancelling/voiding shipments. + +**Supported:** `Yes` (via Karrio resource management) + +- [x] Cancel a newly created shipment +- [x] Shipment status updates to cancelled + +**Notes:** Tested with temporary Bug 1 + Bug 2 workarounds applied (fixes reverted after testing). Cancelled shipment `shp_e405009e929e483299ba69362caa2b04` successfully. Status updated to cancelled via Karrio resource management. + +--- + +## 6. Tracking + +> Test tracking shipment status via `/tracking`. + +**Supported:** `Yes` + +- [x] Track a shipment by tracking number +- [x] Tracking events returned with timestamps +- [x] Tracking status reflects current state + +**Notes:** Tested with temporary Bug 1 + Bug 2 workarounds applied (fixes reverted after testing). Tracked shipment `3627506190` — returned `pending` status with "Label created" event and timestamp. + +--- + +## 7. Pickup Scheduling + +> Test scheduling and managing pickups via `/pickups`. + +**Supported:** `Yes` + +### Schedule Pickup +- [ ] Schedule pickup with valid address and date (Blocked — Bug 2 + Bug 6) +- [ ] Confirmation number returned +- [ ] Pickup appears in pickup list + +### Cancel Pickup +- [ ] Cancel a scheduled pickup +- [ ] Pickup status updates to cancelled + +**Notes:** Attempted but blocked — Bug 2 (invalid typeCode `YP` in `pickup/create.py:159`) causes request to fail, and Bug 6 (missing `additionalDetails`) hides the actual DHL error details. + +--- + +## 8. Return Shipment + +> Test return shipment creation. + +**Supported:** `Yes` + +- [ ] Create return shipment (Blocked — Bugs 1-2 require fixes first) +- [ ] Return label generated +- [ ] Return tracking number provided + +**Notes:** Not tested — blocked by Bug 1 (date parsing) and Bug 2 (invalid typeCode) which must be fixed before return shipments can be created. + +--- + +## Shipping Options Reference + +> Key shipping options available for testing. + +| Option | Type | Description | +|---|---|---| +| `mydhl_saturday_delivery` | bool | Saturday delivery (AA) | +| `mydhl_hold_for_collection` | bool | Hold at service point (LX) | +| `mydhl_duty_tax_paid` | bool | DTP — Duty/Tax Paid (DD) | +| `mydhl_shipment_insurance` | float | Insurance value (II) | +| `mydhl_dangerous_goods` | bool | Dangerous goods (HE) | +| `mydhl_direct_signature` | bool | Direct signature required (SF) | +| `mydhl_paperless_trade` | bool | Paperless trade (WY) | +| `mydhl_gogreen_climate_neutral` | bool | GoGreen Carbon Neutral (EE) | + +--- + +## Edge Cases & Error Handling + +> General robustness checks. + +- [ ] Invalid credentials show clear error message +- [ ] Missing required fields return descriptive validation errors +- [ ] Duplicate shipment creation handled + +**Notes:** Error responses from DHL include `additionalDetails` array with specific validation errors, but the error parser doesn't capture them (see Bug 6). + +--- + +## Bugs Found + +> Bugs discovered during testing. + +### Bug 1: Rate and shipment requests fail with date parsing error +**Impact:** All rate fetching and shipment creation fails without workaround. +**Root Cause:** `lib.fdatetime` called without `current_format` — defaults to `"%Y-%m-%d %H:%M:%S"` but server provides date-only `"%Y-%m-%d"` string. Shipment create also passes output format as positional arg to `current_format`. +**Files:** `rate.py:137`, `shipment/create.py:174` +**Status:** Open + +### Bug 2: Shipment creation fails with invalid package typeCode +**Impact:** All shipment creation fails with `"YP is not a valid enum value"`. +**Root Cause:** `PackagingType` enum uses codes (`FLY`, `YP`, `JB`, `JJ`, `3`) that don't exist in the DHL API spec. Valid codes are: `1CE, 2BC, 2BP, 2BX, 3BX, 4BX, 5BX, 6BX, 7BX, 8BX, CE1, TBL, TBS, WB1, WB2, WB3, WB6, XPD`. Also, `typeCode` is optional — omitting it uses customer packaging by default. +**Files:** `units.py:15-41`, `shipment/create.py:266-268`, `pickup/create.py:159` +**Status:** Open + +### Bug 3: Label content not extracted from shipment response +**Impact:** Shipments are created successfully but label is empty in the response. +**Root Cause:** DHL returns label data (confirmed 7144 chars base64 PDF in raw response) but the `_extract_details` parser returns empty label. Likely a `jstruct.JList` deserialization issue with the `documents` array. +**Files:** `shipment/create.py:46-50` +**Status:** Open + +### Bug 4: Service code mapping mismatches (widespread) +**Impact:** Many rate responses show wrong service names. At least 9 product codes are incorrect or missing from DHL spec reference data. +**Root Cause:** `ShippingService` enum maps product codes to service names that don't match the DHL API v3.1.1 spec. Verified mismatches: `Y` (not in spec, mapped as EXPRESS 9:00), `M` (not in spec, mapped as GLOBALMAIL BUSINESS), `K` (spec: EXPRESS 9:00, mapped as EXPRESS 10:30), `8` (not in spec, mapped as EXPRESS EASY — should be `7`), `Q` (not in spec, mapped as MEDICAL EXPRESS — should be `C`), `R` (spec: GLOBALMAIL, mapped as SPRINTLINE), `G` (spec: ECONOMY SELECT DOMESTIC, mapped as GLOBALMAIL), `E` (not in spec, mapped as BREAKBULK EXPRESS — should be `B`), `F` (not in spec, mapped as EXPRESS FREIGHT). +**Files:** `units.py:44-82` +**Status:** Open + +### Bug 5: Service name in shipment response always shows carrier name +**Impact:** Created shipments show `service: "mydhl"` instead of the actual service like `mydhl_express_worldwide_b2c`. +**Root Cause:** `_extract_details` sets `service=settings.carrier_name` instead of mapping the product code from the response. +**Files:** `shipment/create.py:70` +**Status:** Open + +### Bug 6: Error responses missing additionalDetails +**Impact:** DHL validation errors show `"Multiple problems found, see Additional Details"` but the actual details are not included in the error response. +**Root Cause:** `ErrorResponseType` schema doesn't include `additionalDetails` field, and error parser doesn't pass it through. +**Files:** `error_response.py`, `error.py` +**Status:** Open + +--- + +## Summary + +| Feature | Status | +|------------------|---------| +| Connection Setup | Pass | +| Rating | Pass (with Bug 1 fix) | +| Shipping | Partial (Bugs 1-3) | +| Label | Fail (Bug 3) | +| Cancellation | Pass | +| Tracking | Pass | +| Pickup | Blocked (Bugs 2, 6) | +| Return Shipment | Blocked (Bugs 1-2) | + +**Overall Result:** Blocked — 6 bugs found, fixes needed before full testing + +**Additional Notes:** Tested with DE production credentials against `express.api.dhl.com/mydhlapi`. Rating works after Bug 1 workaround. Shipment creation works (tracking number returned) after Bug 1+2 workarounds, but label extraction is broken (Bug 3). Tracking and cancellation work correctly. Pickup blocked by invalid typeCode (Bug 2) and hidden error details (Bug 6). Return shipments and multi-piece/international shipping blocked pending bug fixes. diff --git a/modules/connectors/mydhl/karrio/mappers/mydhl/proxy.py b/modules/connectors/mydhl/karrio/mappers/mydhl/proxy.py index 10e13eefee..092ad29446 100644 --- a/modules/connectors/mydhl/karrio/mappers/mydhl/proxy.py +++ b/modules/connectors/mydhl/karrio/mappers/mydhl/proxy.py @@ -68,7 +68,7 @@ def create_shipment(self, request: lib.Serializable) -> lib.Deserializable[str]: return lib.Deserializable( shipment_response, lib.to_dict, - dict(paperless_response=paperless_response), + dict(**request.ctx, paperless_response=paperless_response), ) def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[str]: diff --git a/modules/connectors/mydhl/karrio/providers/mydhl/error.py b/modules/connectors/mydhl/karrio/providers/mydhl/error.py index 0e36b15aa6..032536d3ed 100644 --- a/modules/connectors/mydhl/karrio/providers/mydhl/error.py +++ b/modules/connectors/mydhl/karrio/providers/mydhl/error.py @@ -30,6 +30,7 @@ def parse_error_response( **kwargs, "instance": error.instance, "title": error.title, + "additionalDetails": error.additionalDetails, } ), ) diff --git a/modules/connectors/mydhl/karrio/providers/mydhl/pickup/create.py b/modules/connectors/mydhl/karrio/providers/mydhl/pickup/create.py index 7404262f58..072f1bf5b1 100644 --- a/modules/connectors/mydhl/karrio/providers/mydhl/pickup/create.py +++ b/modules/connectors/mydhl/karrio/providers/mydhl/pickup/create.py @@ -138,7 +138,6 @@ def pickup_request( addressLine2=address.address_line2, countyName=address.suburb, provinceName=address.state_name, - countryName=address.country_name, ), contactInformation=pickup_req.ContactInformationType( email=address.email, @@ -156,7 +155,11 @@ def pickup_request( unitOfMeasurement="metric", packages=[ pickup_req.PackageType( - typeCode=provider_units.PackagingType.map(package.packaging_type or "your_packaging").value, + typeCode=lib.identity( + provider_units.PackagingType.map(package.packaging_type).value + if package.packaging_type + else None + ), weight=package.weight.value, dimensions=pickup_req.DimensionsType( length=package.length.value, diff --git a/modules/connectors/mydhl/karrio/providers/mydhl/pickup/update.py b/modules/connectors/mydhl/karrio/providers/mydhl/pickup/update.py index 694b5d4583..25d59326a4 100644 --- a/modules/connectors/mydhl/karrio/providers/mydhl/pickup/update.py +++ b/modules/connectors/mydhl/karrio/providers/mydhl/pickup/update.py @@ -128,7 +128,6 @@ def pickup_update_request( addressLine2=address.address_line2, countyName=address.suburb, provinceName=address.state_name, - countryName=address.country_name, ), contactInformation=pickup_req.ContactInformationType( email=address.email, @@ -146,7 +145,11 @@ def pickup_update_request( unitOfMeasurement="metric", packages=[ pickup_req.PackageType( - typeCode=provider_units.PackagingType.map(package.packaging_type or "your_packaging").value, + typeCode=lib.identity( + provider_units.PackagingType.map(package.packaging_type).value + if package.packaging_type + else None + ), weight=package.weight.value, dimensions=pickup_req.DimensionsType( length=package.length.value, diff --git a/modules/connectors/mydhl/karrio/providers/mydhl/rate.py b/modules/connectors/mydhl/karrio/providers/mydhl/rate.py index 935cec699a..3d08795e7a 100644 --- a/modules/connectors/mydhl/karrio/providers/mydhl/rate.py +++ b/modules/connectors/mydhl/karrio/providers/mydhl/rate.py @@ -136,6 +136,7 @@ def rate_request( ) planned_date = lib.fdatetime( options.shipment_date.state or datetime.datetime.now(), + current_format="%Y-%m-%d", output_format="%Y-%m-%dT%H:%M:%S GMT+00:00", ) @@ -181,11 +182,11 @@ def rate_request( weight=package.weight.value, dimensions=( mydhl_req.DimensionsType( - length=int(package.length.value) if package.length else None, - width=int(package.width.value) if package.width else None, - height=int(package.height.value) if package.height else None, + length=int(package.length.value) if package.length.value else None, + width=int(package.width.value) if package.width.value else None, + height=int(package.height.value) if package.height.value else None, ) - if any([package.length, package.width, package.height]) + if any([package.length.value, package.width.value, package.height.value]) else None ), ) diff --git a/modules/connectors/mydhl/karrio/providers/mydhl/shipment/create.py b/modules/connectors/mydhl/karrio/providers/mydhl/shipment/create.py index 72d7273ae0..1aa13b91d2 100644 --- a/modules/connectors/mydhl/karrio/providers/mydhl/shipment/create.py +++ b/modules/connectors/mydhl/karrio/providers/mydhl/shipment/create.py @@ -29,6 +29,7 @@ def parse_shipment_response( _extract_details( lib.to_object(mydhl_res.ShipmentResponseType, response), settings, + ctx=_response.ctx, ) if response.get("status") is None and response.get("shipmentTrackingNumber") is not None @@ -41,14 +42,33 @@ def parse_shipment_response( def _extract_details( shipment: mydhl_res.ShipmentResponseType, settings: provider_utils.Settings, + ctx: dict = None, ) -> models.ShipmentDetails: tracking_number = str(shipment.shipmentTrackingNumber or "") + + # Collect labels from top-level documents and per-package documents + top_level_labels = [ + doc.content for doc in (shipment.documents or []) + if doc and doc.content + ] + package_labels = [ + doc.content + for pkg in (shipment.packages or []) + if pkg and pkg.documents + for doc in pkg.documents + if doc and doc.content + ] + labels = top_level_labels or package_labels label_doc = next( (doc for doc in (shipment.documents or []) if doc and doc.content), None, ) - label = label_doc.content if label_doc else "" label_format = label_doc.imageFormat if label_doc else "PDF" + label = lib.identity( + labels[0] if len(labels) == 1 + else lib.bundle_base64(labels, label_format) if len(labels) > 1 + else "" + ) package_tracking_numbers = [ pkg.trackingNumber for pkg in (shipment.packages or []) @@ -67,7 +87,9 @@ def _extract_details( models.RateDetails( carrier_id=settings.carrier_id, carrier_name=settings.carrier_name, - service=settings.carrier_name, + service=provider_units.ShippingService.map( + (ctx or {}).get("service") + ).name_or_key, total_charge=lib.to_money(shipment_charge.price), currency=shipment_charge.priceCurrency, extra_charges=[ @@ -173,7 +195,8 @@ def shipment_request( # Incoterm and planned shipping date planned_date = lib.fdatetime( options.shipment_date.state or datetime.datetime.now(), - "%Y-%m-%dT%H:%M:%S GMT+00:00", + current_format="%Y-%m-%d", + output_format="%Y-%m-%dT%H:%M:%S GMT+00:00", ) planned_ship_date = lib.fdate( options.shipment_date.state or datetime.datetime.now() @@ -262,9 +285,11 @@ def shipment_request( content=mydhl_req.ContentType( packages=[ mydhl_req.PackageType( - typeCode=provider_units.PackagingType.map( - package.packaging_type or "your_packaging" - ).value, + typeCode=lib.identity( + provider_units.PackagingType.map(package.packaging_type).value + if package.packaging_type + else None + ), weight=package.weight.value, dimensions=lib.identity( mydhl_req.DimensionsType( @@ -370,5 +395,5 @@ def shipment_request( shipment=lib.to_dict(req["shipment"]), paperless=lib.to_dict(req["paperless"]) if req.get("paperless") else None, ), - dict(is_paperless=is_paperless), + dict(is_paperless=is_paperless, service=payload.service), ) diff --git a/modules/connectors/mydhl/karrio/providers/mydhl/units.py b/modules/connectors/mydhl/karrio/providers/mydhl/units.py index d821a72343..bec6a3b412 100644 --- a/modules/connectors/mydhl/karrio/providers/mydhl/units.py +++ b/modules/connectors/mydhl/karrio/providers/mydhl/units.py @@ -63,7 +63,9 @@ class ShippingService(lib.StrEnum): # Domestic Express Services mydhl_express_domestic = "N" mydhl_express_domestic_12_00 = "1" + mydhl_express_domestic_10_30 = "O" mydhl_express_domestic_9_00 = "I" + mydhl_medical_express_domestic = "C" mydhl_same_day = "S" # Economy/Freight Services diff --git a/modules/connectors/mydhl/karrio/schemas/mydhl/error_response.py b/modules/connectors/mydhl/karrio/schemas/mydhl/error_response.py index 4b40603126..77369ad6da 100644 --- a/modules/connectors/mydhl/karrio/schemas/mydhl/error_response.py +++ b/modules/connectors/mydhl/karrio/schemas/mydhl/error_response.py @@ -9,4 +9,5 @@ class ErrorResponseType: detail: typing.Optional[str] = None title: typing.Optional[str] = None message: typing.Optional[str] = None + additionalDetails: typing.Optional[typing.List[str]] = None status: typing.Optional[int] = None diff --git a/modules/connectors/mydhl/tests/mydhl/test_pickup.py b/modules/connectors/mydhl/tests/mydhl/test_pickup.py index fed7e36c51..343a1fd1d4 100644 --- a/modules/connectors/mydhl/tests/mydhl/test_pickup.py +++ b/modules/connectors/mydhl/tests/mydhl/test_pickup.py @@ -123,7 +123,6 @@ def test_parse_error_response(self): "addressLine1": "123 Main Street", "cityName": "Los Angeles", "countryCode": "US", - "countryName": "United States", "postalCode": "90001", "provinceCode": "CA" } diff --git a/modules/connectors/mydhl/tests/mydhl/test_shipment.py b/modules/connectors/mydhl/tests/mydhl/test_shipment.py index 0bb071259b..838d1dfc62 100644 --- a/modules/connectors/mydhl/tests/mydhl/test_shipment.py +++ b/modules/connectors/mydhl/tests/mydhl/test_shipment.py @@ -137,7 +137,6 @@ def test_parse_error_response(self): "content": { "packages": [ { - "typeCode": "YP", "weight": 5.0, "dimensions": {"length": 25, "width": 20, "height": 15} }