diff --git a/modules/connectors/ups/karrio/mappers/ups/proxy.py b/modules/connectors/ups/karrio/mappers/ups/proxy.py index 15707479bc..d85f38010d 100644 --- a/modules/connectors/ups/karrio/mappers/ups/proxy.py +++ b/modules/connectors/ups/karrio/mappers/ups/proxy.py @@ -1,5 +1,6 @@ import typing import datetime +import uuid import karrio.lib as lib import karrio.api.proxy as proxy import karrio.core.errors as errors @@ -94,20 +95,31 @@ def cancel_shipment(self, request: lib.Serializable) -> lib.Deserializable: def get_tracking(self, request: lib.Serializable) -> lib.Deserializable[typing.List[typing.Tuple[str, dict]]]: locale = self.settings.connection_config.locale.state or "en_US" token = self.get_token() + transaction_source = "karrio-test" if self.settings.test_mode else "karrio-prod" - responses = lib.run_concurently( - lambda tracking_number: ( + def fetch_tracking(tracking_number: str) -> typing.Tuple[str, typing.Any]: + trans_id = str(uuid.uuid4()) + + return ( tracking_number, lib.request( - url=f"{self.settings.server_url}/api/track/v1/details/{tracking_number}?locale={locale}&returnSignature=true", + url=( + f"{self.settings.server_url}/api/track/v1/details/{tracking_number}" + f"?locale={locale}&returnSignature=true" + ), trace=self.trace_as("json"), method="GET", headers={ "authorization": f"Bearer {token}", "content-Type": "application/json", + "transId": trans_id, + "transactionSrc": transaction_source, }, ), - ), + ) + + responses = lib.run_concurently( + fetch_tracking, request.serialize(), ) diff --git a/modules/connectors/ups/tests/ups/test_tracking.py b/modules/connectors/ups/tests/ups/test_tracking.py index 06bdbb60af..e24043260b 100644 --- a/modules/connectors/ups/tests/ups/test_tracking.py +++ b/modules/connectors/ups/tests/ups/test_tracking.py @@ -1,4 +1,5 @@ import unittest +import uuid from unittest.mock import patch from karrio.core.utils import DP from karrio.core.models import TrackingRequest @@ -21,10 +22,14 @@ def test_get_tracking(self, http_mock): karrio.Tracking.fetch(self.TrackingRequest).from_(gateway) url = http_mock.call_args[1]["url"] + headers = http_mock.call_args[1]["headers"] self.assertEqual( url, f"{gateway.settings.server_url}/api/track/v1/details/{self.TrackingRequest.tracking_numbers[0]}?locale=en_US&returnSignature=true", ) + self.assertEqual(headers["transactionSrc"], "karrio-prod") + self.assertTrue(headers["transId"]) + uuid.UUID(headers["transId"]) def test_tracking_auth_error_parsing(self): with patch("karrio.mappers.ups.proxy.lib.request") as mock: diff --git a/modules/core/karrio/server/core/serializers.py b/modules/core/karrio/server/core/serializers.py index bfd5bf6ed5..a6b052aa38 100644 --- a/modules/core/karrio/server/core/serializers.py +++ b/modules/core/karrio/server/core/serializers.py @@ -847,6 +847,23 @@ class TrackingData(serializers.Serializer): required=True, help_text="The tracking carrier", ) + carrier_id = serializers.CharField( + required=False, + allow_blank=False, + allow_null=True, + help_text=( + "Optional carrier connection identifier (connection id or carrier_id). " + "Useful when multiple connections exist for the same carrier." + ), + ) + test_mode = serializers.BooleanField( + required=False, + allow_null=True, + help_text=( + "Optional connection mode selector. When provided, tracking resolves " + "against carriers in the specified test/prod mode." + ), + ) account_number = serializers.CharField( required=False, allow_blank=True, diff --git a/modules/manager/karrio/server/manager/tests/test_trackers.py b/modules/manager/karrio/server/manager/tests/test_trackers.py index 5387c048cc..8e88fc10b0 100644 --- a/modules/manager/karrio/server/manager/tests/test_trackers.py +++ b/modules/manager/karrio/server/manager/tests/test_trackers.py @@ -44,6 +44,34 @@ def test_shipment_tracking_retry(self): self.assertDictEqual(response_data, TRACKING_RESPONSE) self.assertEqual(len(self.user.tracking_set.all()), 1) + def test_trackers_post_with_connection_selectors(self): + url = reverse("karrio.server.manager:trackers-list") + data = dict( + tracking_number="1Z12345E6205277936", + carrier_name="ups", + carrier_id="ups_package", + test_mode=True, + ) + + with ( + patch( + "karrio.server.manager.serializers.tracking.Connections.first", + wraps=serializers.tracking.Connections.first, + ) as first_mock, + patch("karrio.server.core.gateway.utils.identity") as mock, + ): + mock.return_value = RETURNED_VALUE + response = self.client.post(url, data) + + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + self.assertTrue( + any( + call.kwargs.get("carrier_id") == "ups_package" + and call.kwargs.get("test_mode") is True + for call in first_mock.call_args_list + ) + ) + class TestTrackersUpdate(APITestCase): def setUp(self) -> None: diff --git a/modules/manager/karrio/server/manager/views/trackers.py b/modules/manager/karrio/server/manager/views/trackers.py index 0d2750ad38..cf7c28f709 100644 --- a/modules/manager/karrio/server/manager/views/trackers.py +++ b/modules/manager/karrio/server/manager/views/trackers.py @@ -126,6 +126,12 @@ def post(self, request: Request): # If a hub is specified, use the hub as carrier to track the package "carrier_name": carrier_name, } + # Respect explicit connection selectors from payload when present. + if data.get("carrier_id"): + carrier_filter["carrier_id"] = data["carrier_id"] + if data.get("test_mode") is not None: + # Preserve explicit False (prod) and True (test). + carrier_filter["test_mode"] = data["test_mode"] data = { **data, "tracking_number": data["tracking_number"], diff --git a/modules/proxy/karrio/server/proxy/tests/test_tracking.py b/modules/proxy/karrio/server/proxy/tests/test_tracking.py index 229e14b1d8..1ed4a46a98 100644 --- a/modules/proxy/karrio/server/proxy/tests/test_tracking.py +++ b/modules/proxy/karrio/server/proxy/tests/test_tracking.py @@ -4,6 +4,7 @@ from rest_framework import status from karrio.core.models import TrackingDetails, TrackingEvent from karrio.server.core.tests import APITestCase +from karrio.server.core.gateway import Shipments class TestTracking(APITestCase): @@ -19,6 +20,29 @@ def test_tracking_shipment(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertDictEqual(response_data, TRACKING_RESPONSE) + def test_tracking_shipment_with_connection_selectors(self): + url = reverse("karrio.server.proxy:get-tracking") + data = dict( + tracking_number="1Z12345E6205277936", + carrier_name="ups", + carrier_id="ups_package", + test_mode=True, + ) + + with ( + patch( + "karrio.server.proxy.views.tracking.Shipments.track", + wraps=Shipments.track, + ) as track_mock, + patch("karrio.server.core.gateway.utils.identity") as mock, + ): + mock.return_value = RETURNED_VALUE + response = self.client.post(f"{url}", data) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(track_mock.call_args.kwargs.get("carrier_id"), "ups_package") + self.assertIs(track_mock.call_args.kwargs.get("test_mode"), True) + RETURNED_VALUE = [ [ diff --git a/modules/proxy/karrio/server/proxy/views/tracking.py b/modules/proxy/karrio/server/proxy/views/tracking.py index 44a2e2f9de..de264f89dd 100644 --- a/modules/proxy/karrio/server/proxy/views/tracking.py +++ b/modules/proxy/karrio/server/proxy/views/tracking.py @@ -57,6 +57,12 @@ def post(self, request: Request): query.get("hub") if "hub" in query else data["carrier_name"] ), } + # Respect explicit connection selectors from payload when present. + if data.get("carrier_id"): + carrier_filter["carrier_id"] = data["carrier_id"] + if data.get("test_mode") is not None: + # Preserve explicit False (prod) and True (test). + carrier_filter["test_mode"] = data["test_mode"] data = { **data, "tracking_numbers": [data["tracking_number"]],