karrio-server-manager 2025.5.1__py3-none-any.whl → 2025.5.2__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.
- karrio/server/manager/serializers/shipment.py +112 -10
- karrio/server/manager/tests/test_shipments.py +255 -182
- {karrio_server_manager-2025.5.1.dist-info → karrio_server_manager-2025.5.2.dist-info}/METADATA +1 -1
- {karrio_server_manager-2025.5.1.dist-info → karrio_server_manager-2025.5.2.dist-info}/RECORD +6 -6
- {karrio_server_manager-2025.5.1.dist-info → karrio_server_manager-2025.5.2.dist-info}/WHEEL +0 -0
- {karrio_server_manager-2025.5.1.dist-info → karrio_server_manager-2025.5.2.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import uuid
|
|
1
2
|
import typing
|
|
2
3
|
import rest_framework.status as status
|
|
3
4
|
import django.db.transaction as transaction
|
|
@@ -129,20 +130,33 @@ class ShipmentSerializer(ShipmentData):
|
|
|
129
130
|
def create(
|
|
130
131
|
self, validated_data: dict, context: Context, **kwargs
|
|
131
132
|
) -> models.Shipment:
|
|
133
|
+
# fmt: off
|
|
132
134
|
service = validated_data.get("service")
|
|
133
135
|
carrier_ids = validated_data.get("carrier_ids") or []
|
|
134
136
|
fetch_rates = validated_data.get("fetch_rates") is not False
|
|
135
137
|
services = [service] if service is not None else validated_data.get("services")
|
|
138
|
+
options = validated_data.get("options") or {}
|
|
139
|
+
|
|
140
|
+
# Check if we should skip rate fetching for has_alternative_services
|
|
141
|
+
skip_rate_fetching, resolved_carrier_name, _ = (
|
|
142
|
+
resolve_alternative_service_carrier(
|
|
143
|
+
service=service,
|
|
144
|
+
carrier_ids=carrier_ids,
|
|
145
|
+
carriers=[], # Pre-check before loading carriers
|
|
146
|
+
options=options,
|
|
147
|
+
context=context,
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
136
151
|
carriers = gateway.Carriers.list(
|
|
137
152
|
context=context,
|
|
138
153
|
carrier_ids=carrier_ids,
|
|
139
|
-
**({"
|
|
154
|
+
**({"carrier_name": resolved_carrier_name} if resolved_carrier_name else {}),
|
|
155
|
+
**({"services": services} if any(services) and not skip_rate_fetching else {}),
|
|
140
156
|
**{"raise_not_found": True, **DEFAULT_CARRIER_FILTER},
|
|
141
157
|
)
|
|
142
158
|
payment = validated_data.get("payment") or lib.to_dict(
|
|
143
|
-
datatypes.Payment(
|
|
144
|
-
currency=(validated_data.get("options") or {}).get("currency")
|
|
145
|
-
)
|
|
159
|
+
datatypes.Payment(currency=options.get("currency"))
|
|
146
160
|
)
|
|
147
161
|
rating_data = {
|
|
148
162
|
**validated_data,
|
|
@@ -152,11 +166,11 @@ class ShipmentSerializer(ShipmentData):
|
|
|
152
166
|
messages = validated_data.get("messages") or []
|
|
153
167
|
apply_shipping_rules = lib.identity(
|
|
154
168
|
getattr(conf.settings, "SHIPPING_RULES", False)
|
|
155
|
-
and
|
|
169
|
+
and options.get("apply_shipping_rules", False)
|
|
156
170
|
)
|
|
157
171
|
|
|
158
|
-
# Get live rates
|
|
159
|
-
if fetch_rates or apply_shipping_rules:
|
|
172
|
+
# Get live rates (skip if has_alternative_services is enabled)
|
|
173
|
+
if (fetch_rates or apply_shipping_rules) and not skip_rate_fetching:
|
|
160
174
|
rate_response: datatypes.RateResponse = (
|
|
161
175
|
RateSerializer.map(data=rating_data, context=context)
|
|
162
176
|
.save(carriers=carriers)
|
|
@@ -165,6 +179,16 @@ class ShipmentSerializer(ShipmentData):
|
|
|
165
179
|
rates = lib.to_dict(rate_response.rates)
|
|
166
180
|
messages = lib.to_dict(rate_response.messages)
|
|
167
181
|
|
|
182
|
+
# Create synthetic rate when skipping rate fetching
|
|
183
|
+
if skip_rate_fetching:
|
|
184
|
+
_, _, rates = resolve_alternative_service_carrier(
|
|
185
|
+
service=service,
|
|
186
|
+
carrier_ids=carrier_ids,
|
|
187
|
+
carriers=carriers,
|
|
188
|
+
options=options,
|
|
189
|
+
context=context,
|
|
190
|
+
)
|
|
191
|
+
|
|
168
192
|
shipment = models.Shipment.objects.create(
|
|
169
193
|
**{
|
|
170
194
|
**{
|
|
@@ -220,14 +244,14 @@ class ShipmentSerializer(ShipmentData):
|
|
|
220
244
|
context=context,
|
|
221
245
|
)
|
|
222
246
|
|
|
223
|
-
# Buy label if preferred service is selected
|
|
224
|
-
if (service and fetch_rates) or apply_shipping_rules:
|
|
247
|
+
# Buy label if preferred service is selected, shipping rules applied, or skip rate fetching
|
|
248
|
+
if (service and fetch_rates) or apply_shipping_rules or skip_rate_fetching:
|
|
225
249
|
return buy_shipment_label(
|
|
226
250
|
shipment,
|
|
227
251
|
context=context,
|
|
228
252
|
service=service,
|
|
229
253
|
)
|
|
230
|
-
|
|
254
|
+
# fmt: on
|
|
231
255
|
return shipment
|
|
232
256
|
|
|
233
257
|
@transaction.atomic
|
|
@@ -896,3 +920,81 @@ def upload_customs_forms(shipment: models.Shipment, document: dict, context=None
|
|
|
896
920
|
.save(shipment=shipment)
|
|
897
921
|
.instance
|
|
898
922
|
)
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def resolve_alternative_service_carrier(
|
|
926
|
+
service: str,
|
|
927
|
+
carrier_ids: list,
|
|
928
|
+
carriers: list,
|
|
929
|
+
options: dict,
|
|
930
|
+
context: Context,
|
|
931
|
+
) -> typing.Tuple[bool, typing.Optional[str], typing.List[dict]]:
|
|
932
|
+
"""
|
|
933
|
+
Resolve carrier and create synthetic rate for has_alternative_services flow.
|
|
934
|
+
|
|
935
|
+
When has_alternative_services=True and a service is specified, this function:
|
|
936
|
+
1. Determines if rate fetching should be skipped
|
|
937
|
+
2. Resolves the carrier from the service name
|
|
938
|
+
3. Creates a synthetic rate for direct label purchase
|
|
939
|
+
|
|
940
|
+
Returns:
|
|
941
|
+
Tuple of (skip_rate_fetching, resolved_carrier_name, synthetic_rates)
|
|
942
|
+
"""
|
|
943
|
+
has_alternative_services = options.get("has_alternative_services", False)
|
|
944
|
+
skip_rate_fetching = service is not None and has_alternative_services
|
|
945
|
+
|
|
946
|
+
if not skip_rate_fetching:
|
|
947
|
+
return False, None, []
|
|
948
|
+
|
|
949
|
+
# Resolve carrier from service when no explicit carrier_ids provided
|
|
950
|
+
resolved_carrier_name = None
|
|
951
|
+
if not any(carrier_ids):
|
|
952
|
+
resolved_carrier_name = utils._get_carrier_for_service(service, context=context)
|
|
953
|
+
if resolved_carrier_name is None:
|
|
954
|
+
raise exceptions.APIException(
|
|
955
|
+
f"Could not resolve carrier for service '{service}'",
|
|
956
|
+
code="validation_error",
|
|
957
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
if len(carriers) == 0:
|
|
961
|
+
return skip_rate_fetching, resolved_carrier_name, []
|
|
962
|
+
|
|
963
|
+
# Find carrier connection matching the service's carrier
|
|
964
|
+
carrier_name = resolved_carrier_name or utils._get_carrier_for_service(
|
|
965
|
+
service, context=context
|
|
966
|
+
)
|
|
967
|
+
carrier = lib.identity(
|
|
968
|
+
next(
|
|
969
|
+
(c for c in carriers if c.carrier_name == carrier_name),
|
|
970
|
+
carriers[0] if carrier_name is None else None,
|
|
971
|
+
)
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
if carrier is None:
|
|
975
|
+
raise exceptions.APIException(
|
|
976
|
+
f"No carrier connection found for service '{service}'",
|
|
977
|
+
code="validation_error",
|
|
978
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
# Create synthetic rate for direct label purchase
|
|
982
|
+
synthetic_rates = [
|
|
983
|
+
{
|
|
984
|
+
"id": f"rat_{uuid.uuid4().hex}",
|
|
985
|
+
"carrier_id": carrier.carrier_id,
|
|
986
|
+
"carrier_name": carrier.carrier_name,
|
|
987
|
+
"service": service,
|
|
988
|
+
"currency": options.get("currency") or "USD",
|
|
989
|
+
"total_charge": 0,
|
|
990
|
+
"meta": {
|
|
991
|
+
"carrier_connection_id": carrier.pk,
|
|
992
|
+
"has_alternative_services": True,
|
|
993
|
+
"rate_provider": carrier.carrier_name,
|
|
994
|
+
"service_name": service.upper().replace("_", " "),
|
|
995
|
+
},
|
|
996
|
+
"test_mode": context.test_mode,
|
|
997
|
+
}
|
|
998
|
+
]
|
|
999
|
+
|
|
1000
|
+
return skip_rate_fetching, resolved_carrier_name, synthetic_rates
|
|
@@ -212,6 +212,144 @@ class TestShipmentPurchase(TestShipmentFixture):
|
|
|
212
212
|
).exists()
|
|
213
213
|
)
|
|
214
214
|
|
|
215
|
+
def test_purchase_shipment_with_has_alternative_services(self):
|
|
216
|
+
"""
|
|
217
|
+
Test that when has_alternative_services is enabled and service is requested
|
|
218
|
+
but not in rates, the purchase proceeds by delegating service resolution to the carrier.
|
|
219
|
+
"""
|
|
220
|
+
url = reverse(
|
|
221
|
+
"karrio.server.manager:shipment-purchase",
|
|
222
|
+
kwargs=dict(pk=self.shipment.pk),
|
|
223
|
+
)
|
|
224
|
+
self.shipment.options = {"has_alternative_services": True}
|
|
225
|
+
self.shipment.save()
|
|
226
|
+
data = {"service": "canadapost_expedited_parcel"}
|
|
227
|
+
|
|
228
|
+
with patch("karrio.server.core.gateway.utils.identity") as mock:
|
|
229
|
+
mock.return_value = CREATED_SHIPMENT_RESPONSE
|
|
230
|
+
response = self.client.post(url, data)
|
|
231
|
+
response_data = json.loads(response.content)
|
|
232
|
+
|
|
233
|
+
self.assertResponseNoErrors(response) # type: ignore
|
|
234
|
+
self.assertDictEqual(
|
|
235
|
+
dict(status=response_data["status"], service=response_data["service"]),
|
|
236
|
+
dict(status="purchased", service="canadapost_expedited_parcel"),
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TestSingleCallLabelPurchase(APITestCase):
|
|
241
|
+
"""Test single call label purchase via POST to shipment-list with a service specified."""
|
|
242
|
+
|
|
243
|
+
def test_single_call_label_purchase(self):
|
|
244
|
+
"""
|
|
245
|
+
Test that when a shipment is created with a service specified,
|
|
246
|
+
the label is purchased in a single call after fetching rates.
|
|
247
|
+
"""
|
|
248
|
+
url = reverse("karrio.server.manager:shipment-list")
|
|
249
|
+
data = SINGLE_CALL_LABEL_DATA
|
|
250
|
+
|
|
251
|
+
with patch("karrio.server.core.gateway.utils.identity") as mock:
|
|
252
|
+
mock.side_effect = [RETURNED_RATES_VALUE, CREATED_SHIPMENT_RESPONSE]
|
|
253
|
+
response = self.client.post(url, data)
|
|
254
|
+
response_data = json.loads(response.content)
|
|
255
|
+
|
|
256
|
+
self.assertResponseNoErrors(response) # type: ignore
|
|
257
|
+
self.assertDictEqual(
|
|
258
|
+
{
|
|
259
|
+
"status": response_data["status"],
|
|
260
|
+
"carrier_name": response_data["carrier_name"],
|
|
261
|
+
"service": response_data["service"],
|
|
262
|
+
"tracking_number": response_data["tracking_number"],
|
|
263
|
+
"services": response_data["services"],
|
|
264
|
+
"rates_count": len(response_data["rates"]),
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"status": "purchased",
|
|
268
|
+
"carrier_name": "canadapost",
|
|
269
|
+
"service": "canadapost_priority",
|
|
270
|
+
"tracking_number": "123456789012",
|
|
271
|
+
"services": ["canadapost_priority"],
|
|
272
|
+
"rates_count": 1,
|
|
273
|
+
},
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class TestSingleCallWithAlternativeServices(APITestCase):
|
|
278
|
+
"""Test single call label purchase with has_alternative_services flag (skip rate fetching)."""
|
|
279
|
+
|
|
280
|
+
def test_single_call_label_purchase_skip_rates(self):
|
|
281
|
+
"""
|
|
282
|
+
Test that when has_alternative_services=True and service is specified,
|
|
283
|
+
rate fetching is skipped and label is purchased directly.
|
|
284
|
+
Carrier is resolved from the service name.
|
|
285
|
+
"""
|
|
286
|
+
url = reverse("karrio.server.manager:shipment-list")
|
|
287
|
+
data = SINGLE_CALL_SKIP_RATES_DATA
|
|
288
|
+
|
|
289
|
+
with patch("karrio.server.core.gateway.utils.identity") as mock:
|
|
290
|
+
mock.return_value = CREATED_SHIPMENT_RESPONSE
|
|
291
|
+
response = self.client.post(url, data)
|
|
292
|
+
response_data = json.loads(response.content)
|
|
293
|
+
|
|
294
|
+
# Verify only 1 call was made (rates were skipped)
|
|
295
|
+
self.assertEqual(mock.call_count, 1)
|
|
296
|
+
|
|
297
|
+
self.assertResponseNoErrors(response) # type: ignore
|
|
298
|
+
self.assertDictEqual(
|
|
299
|
+
{
|
|
300
|
+
"status": response_data["status"],
|
|
301
|
+
"carrier_name": response_data["carrier_name"],
|
|
302
|
+
"service": response_data["service"],
|
|
303
|
+
"tracking_number": response_data["tracking_number"],
|
|
304
|
+
"has_alternative_services": response_data["selected_rate"]["meta"].get(
|
|
305
|
+
"has_alternative_services"
|
|
306
|
+
),
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"status": "purchased",
|
|
310
|
+
"carrier_name": "canadapost",
|
|
311
|
+
"service": "canadapost_priority",
|
|
312
|
+
"tracking_number": "123456789012",
|
|
313
|
+
"has_alternative_services": True,
|
|
314
|
+
},
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def test_single_call_label_purchase_skip_rates_with_carrier_ids(self):
|
|
318
|
+
"""
|
|
319
|
+
Test that when has_alternative_services=True, service, and carrier_ids are specified,
|
|
320
|
+
rate fetching is skipped and label is purchased directly.
|
|
321
|
+
"""
|
|
322
|
+
url = reverse("karrio.server.manager:shipment-list")
|
|
323
|
+
data = {**SINGLE_CALL_SKIP_RATES_DATA, "carrier_ids": ["canadapost"]}
|
|
324
|
+
|
|
325
|
+
with patch("karrio.server.core.gateway.utils.identity") as mock:
|
|
326
|
+
mock.return_value = CREATED_SHIPMENT_RESPONSE
|
|
327
|
+
response = self.client.post(url, data)
|
|
328
|
+
response_data = json.loads(response.content)
|
|
329
|
+
|
|
330
|
+
# Verify only 1 call was made (rates were skipped)
|
|
331
|
+
self.assertEqual(mock.call_count, 1)
|
|
332
|
+
|
|
333
|
+
self.assertResponseNoErrors(response) # type: ignore
|
|
334
|
+
self.assertDictEqual(
|
|
335
|
+
{
|
|
336
|
+
"status": response_data["status"],
|
|
337
|
+
"carrier_name": response_data["carrier_name"],
|
|
338
|
+
"service": response_data["service"],
|
|
339
|
+
"tracking_number": response_data["tracking_number"],
|
|
340
|
+
"has_alternative_services": response_data["selected_rate"]["meta"].get(
|
|
341
|
+
"has_alternative_services"
|
|
342
|
+
),
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"status": "purchased",
|
|
346
|
+
"carrier_name": "canadapost",
|
|
347
|
+
"service": "canadapost_priority",
|
|
348
|
+
"tracking_number": "123456789012",
|
|
349
|
+
"has_alternative_services": True,
|
|
350
|
+
},
|
|
351
|
+
)
|
|
352
|
+
|
|
215
353
|
|
|
216
354
|
SHIPMENT_DATA = {
|
|
217
355
|
"recipient": {
|
|
@@ -260,11 +398,21 @@ SHIPMENT_RATES = {
|
|
|
260
398
|
"total_charge": 106.71,
|
|
261
399
|
"transit_days": 2,
|
|
262
400
|
"extra_charges": [
|
|
263
|
-
{
|
|
401
|
+
{
|
|
402
|
+
"name": "Duty and taxes",
|
|
403
|
+
"amount": 13.92,
|
|
404
|
+
"currency": "CAD",
|
|
405
|
+
"id": ANY,
|
|
406
|
+
},
|
|
264
407
|
{"name": "Fuel surcharge", "amount": 2.7, "currency": "CAD", "id": ANY},
|
|
265
408
|
{"name": "SMB Savings", "amount": -11.74, "currency": "CAD", "id": ANY},
|
|
266
409
|
{"name": "Discount", "amount": -9.04, "currency": "CAD", "id": ANY},
|
|
267
|
-
{
|
|
410
|
+
{
|
|
411
|
+
"name": "Base surcharge",
|
|
412
|
+
"amount": 101.83,
|
|
413
|
+
"currency": "CAD",
|
|
414
|
+
"id": ANY,
|
|
415
|
+
},
|
|
268
416
|
],
|
|
269
417
|
"meta": {
|
|
270
418
|
"ext": "canadapost",
|
|
@@ -535,7 +683,12 @@ PURCHASED_SHIPMENT = {
|
|
|
535
683
|
{"name": "Fuel surcharge", "amount": 2.7, "currency": "CAD", "id": ANY},
|
|
536
684
|
{"name": "SMB Savings", "amount": -11.74, "currency": "CAD", "id": ANY},
|
|
537
685
|
{"name": "Discount", "amount": -9.04, "currency": "CAD", "id": ANY},
|
|
538
|
-
{
|
|
686
|
+
{
|
|
687
|
+
"name": "Duties and taxes",
|
|
688
|
+
"amount": 13.92,
|
|
689
|
+
"currency": "CAD",
|
|
690
|
+
"id": ANY,
|
|
691
|
+
},
|
|
539
692
|
],
|
|
540
693
|
"meta": {
|
|
541
694
|
"service_name": "CANADAPOST PRIORITY",
|
|
@@ -679,11 +832,31 @@ CANCEL_RESPONSE = {
|
|
|
679
832
|
"total_charge": 106.71,
|
|
680
833
|
"transit_days": 2,
|
|
681
834
|
"extra_charges": [
|
|
682
|
-
{
|
|
683
|
-
|
|
684
|
-
|
|
835
|
+
{
|
|
836
|
+
"name": "Base charge",
|
|
837
|
+
"amount": 101.83,
|
|
838
|
+
"currency": "CAD",
|
|
839
|
+
"id": None,
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
"name": "Fuel surcharge",
|
|
843
|
+
"amount": 2.7,
|
|
844
|
+
"currency": "CAD",
|
|
845
|
+
"id": None,
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
"name": "SMB Savings",
|
|
849
|
+
"amount": -11.74,
|
|
850
|
+
"currency": "CAD",
|
|
851
|
+
"id": None,
|
|
852
|
+
},
|
|
685
853
|
{"name": "Discount", "amount": -9.04, "currency": "CAD", "id": None},
|
|
686
|
-
{
|
|
854
|
+
{
|
|
855
|
+
"name": "Duties and taxes",
|
|
856
|
+
"amount": 13.92,
|
|
857
|
+
"currency": "CAD",
|
|
858
|
+
"id": None,
|
|
859
|
+
},
|
|
687
860
|
],
|
|
688
861
|
"meta": {
|
|
689
862
|
"carrier_connection_id": ANY,
|
|
@@ -797,11 +970,31 @@ CANCEL_PURCHASED_RESPONSE = {
|
|
|
797
970
|
"total_charge": 106.71,
|
|
798
971
|
"transit_days": 2,
|
|
799
972
|
"extra_charges": [
|
|
800
|
-
{
|
|
801
|
-
|
|
802
|
-
|
|
973
|
+
{
|
|
974
|
+
"name": "Base charge",
|
|
975
|
+
"amount": 101.83,
|
|
976
|
+
"currency": "CAD",
|
|
977
|
+
"id": None,
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
"name": "Fuel surcharge",
|
|
981
|
+
"amount": 2.7,
|
|
982
|
+
"currency": "CAD",
|
|
983
|
+
"id": None,
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
"name": "SMB Savings",
|
|
987
|
+
"amount": -11.74,
|
|
988
|
+
"currency": "CAD",
|
|
989
|
+
"id": None,
|
|
990
|
+
},
|
|
803
991
|
{"name": "Discount", "amount": -9.04, "currency": "CAD", "id": None},
|
|
804
|
-
{
|
|
992
|
+
{
|
|
993
|
+
"name": "Duties and taxes",
|
|
994
|
+
"amount": 13.92,
|
|
995
|
+
"currency": "CAD",
|
|
996
|
+
"id": None,
|
|
997
|
+
},
|
|
805
998
|
],
|
|
806
999
|
"meta": {
|
|
807
1000
|
"carrier_connection_id": ANY,
|
|
@@ -832,194 +1025,74 @@ CANCEL_PURCHASED_RESPONSE = {
|
|
|
832
1025
|
"invoice_url": None,
|
|
833
1026
|
}
|
|
834
1027
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
super().setUp()
|
|
839
|
-
carrier = providers.Carrier.objects.get(carrier_id="canadapost")
|
|
840
|
-
# Rates have "canadapost_regular_parcel" but we'll request "canadapost_priority"
|
|
841
|
-
self.shipment.rates = [
|
|
842
|
-
{
|
|
843
|
-
"id": "rat_alt_service_test",
|
|
844
|
-
"carrier_id": "canadapost",
|
|
845
|
-
"carrier_name": "canadapost",
|
|
846
|
-
"currency": "CAD",
|
|
847
|
-
"estimated_delivery": None,
|
|
848
|
-
"extra_charges": [
|
|
849
|
-
{"amount": 50.00, "currency": "CAD", "name": "Base charge"},
|
|
850
|
-
],
|
|
851
|
-
"service": "canadapost_regular_parcel",
|
|
852
|
-
"total_charge": 50.00,
|
|
853
|
-
"transit_days": 5,
|
|
854
|
-
"test_mode": True,
|
|
855
|
-
"meta": {
|
|
856
|
-
"rate_provider": "canadapost",
|
|
857
|
-
"service_name": "CANADAPOST REGULAR PARCEL",
|
|
858
|
-
"carrier_connection_id": carrier.pk,
|
|
859
|
-
},
|
|
860
|
-
}
|
|
861
|
-
]
|
|
862
|
-
self.shipment.options = {"has_alternative_services": True}
|
|
863
|
-
self.shipment.save()
|
|
864
|
-
|
|
865
|
-
def test_purchase_with_alternative_service(self):
|
|
866
|
-
"""
|
|
867
|
-
Test that when canadapost_priority is requested but only canadapost_regular_parcel
|
|
868
|
-
is in rates, the purchase proceeds with has_alternative_services=True,
|
|
869
|
-
delegating service resolution to the carrier.
|
|
870
|
-
"""
|
|
871
|
-
url = reverse(
|
|
872
|
-
"karrio.server.manager:shipment-purchase",
|
|
873
|
-
kwargs=dict(pk=self.shipment.pk),
|
|
874
|
-
)
|
|
875
|
-
data = {"service": "canadapost_priority"}
|
|
876
|
-
|
|
877
|
-
with patch("karrio.server.core.gateway.utils.identity") as mock:
|
|
878
|
-
mock.return_value = CREATED_SHIPMENT_RESPONSE
|
|
879
|
-
response = self.client.post(url, data)
|
|
880
|
-
response_data = json.loads(response.content)
|
|
881
|
-
|
|
882
|
-
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
883
|
-
self.assertDictEqual(response_data, ALTERNATIVE_SERVICE_PURCHASED_SHIPMENT)
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
ALTERNATIVE_SERVICE_PURCHASED_SHIPMENT = {
|
|
887
|
-
"id": ANY,
|
|
888
|
-
"object_type": "shipment",
|
|
889
|
-
"tracking_url": "/v1/trackers/canadapost/123456789012",
|
|
890
|
-
"shipper": {
|
|
891
|
-
"id": ANY,
|
|
892
|
-
"postal_code": "E1C4Z8",
|
|
893
|
-
"city": "Moncton",
|
|
894
|
-
"federal_tax_id": None,
|
|
895
|
-
"state_tax_id": None,
|
|
1028
|
+
SINGLE_CALL_LABEL_DATA = {
|
|
1029
|
+
"recipient": {
|
|
1030
|
+
"address_line1": "125 Church St",
|
|
896
1031
|
"person_name": "John Poop",
|
|
897
1032
|
"company_name": "A corp.",
|
|
898
|
-
"country_code": "CA",
|
|
899
|
-
"email": None,
|
|
900
1033
|
"phone_number": "514 000 0000",
|
|
901
|
-
"
|
|
902
|
-
"
|
|
1034
|
+
"city": "Moncton",
|
|
1035
|
+
"country_code": "CA",
|
|
1036
|
+
"postal_code": "E1C4Z8",
|
|
903
1037
|
"residential": False,
|
|
904
|
-
"
|
|
905
|
-
"address_line2": None,
|
|
906
|
-
"validate_location": False,
|
|
907
|
-
"object_type": "address",
|
|
908
|
-
"validation": None,
|
|
1038
|
+
"state_code": "NB",
|
|
909
1039
|
},
|
|
910
|
-
"
|
|
911
|
-
"
|
|
912
|
-
"postal_code": "V6M2V9",
|
|
913
|
-
"city": "Vancouver",
|
|
914
|
-
"federal_tax_id": None,
|
|
915
|
-
"state_tax_id": None,
|
|
1040
|
+
"shipper": {
|
|
1041
|
+
"address_line1": "5840 Oak St",
|
|
916
1042
|
"person_name": "Jane Doe",
|
|
917
1043
|
"company_name": "B corp.",
|
|
918
|
-
"country_code": "CA",
|
|
919
|
-
"email": None,
|
|
920
1044
|
"phone_number": "514 000 9999",
|
|
921
|
-
"
|
|
922
|
-
"
|
|
1045
|
+
"city": "Vancouver",
|
|
1046
|
+
"country_code": "CA",
|
|
1047
|
+
"postal_code": "V6M2V9",
|
|
923
1048
|
"residential": False,
|
|
924
|
-
"
|
|
925
|
-
"address_line2": None,
|
|
926
|
-
"validate_location": False,
|
|
927
|
-
"object_type": "address",
|
|
928
|
-
"validation": None,
|
|
1049
|
+
"state_code": "BC",
|
|
929
1050
|
},
|
|
930
1051
|
"parcels": [
|
|
931
1052
|
{
|
|
932
|
-
"
|
|
933
|
-
"weight": 1.0,
|
|
934
|
-
"width": None,
|
|
935
|
-
"height": None,
|
|
936
|
-
"length": None,
|
|
937
|
-
"packaging_type": None,
|
|
938
|
-
"package_preset": "canadapost_corrugated_small_box",
|
|
939
|
-
"description": None,
|
|
940
|
-
"content": None,
|
|
941
|
-
"is_document": False,
|
|
1053
|
+
"weight": 1,
|
|
942
1054
|
"weight_unit": "KG",
|
|
943
|
-
"
|
|
944
|
-
"items": [],
|
|
945
|
-
"freight_class": None,
|
|
946
|
-
"reference_number": ANY,
|
|
947
|
-
"object_type": "parcel",
|
|
948
|
-
"options": {},
|
|
1055
|
+
"package_preset": "canadapost_corrugated_small_box",
|
|
949
1056
|
}
|
|
950
1057
|
],
|
|
951
|
-
"
|
|
952
|
-
"
|
|
953
|
-
"
|
|
954
|
-
"
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1058
|
+
"payment": {"currency": "CAD", "paid_by": "sender"},
|
|
1059
|
+
"service": "canadapost_priority",
|
|
1060
|
+
"carrier_ids": ["canadapost"],
|
|
1061
|
+
"options": {"insurance": 100},
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
SINGLE_CALL_SKIP_RATES_DATA = {
|
|
1065
|
+
# Note: No carrier_ids provided - carrier is resolved from service name
|
|
1066
|
+
"recipient": {
|
|
1067
|
+
"address_line1": "125 Church St",
|
|
1068
|
+
"person_name": "John Poop",
|
|
1069
|
+
"company_name": "A corp.",
|
|
1070
|
+
"phone_number": "514 000 0000",
|
|
1071
|
+
"city": "Moncton",
|
|
1072
|
+
"country_code": "CA",
|
|
1073
|
+
"postal_code": "E1C4Z8",
|
|
1074
|
+
"residential": False,
|
|
1075
|
+
"state_code": "NB",
|
|
1076
|
+
},
|
|
1077
|
+
"shipper": {
|
|
1078
|
+
"address_line1": "5840 Oak St",
|
|
1079
|
+
"person_name": "Jane Doe",
|
|
1080
|
+
"company_name": "B corp.",
|
|
1081
|
+
"phone_number": "514 000 9999",
|
|
1082
|
+
"city": "Vancouver",
|
|
1083
|
+
"country_code": "CA",
|
|
1084
|
+
"postal_code": "V6M2V9",
|
|
1085
|
+
"residential": False,
|
|
1086
|
+
"state_code": "BC",
|
|
1087
|
+
},
|
|
1088
|
+
"parcels": [
|
|
958
1089
|
{
|
|
959
|
-
"
|
|
960
|
-
"
|
|
961
|
-
"
|
|
962
|
-
"carrier_id": "canadapost",
|
|
963
|
-
"currency": "CAD",
|
|
964
|
-
"estimated_delivery": ANY,
|
|
965
|
-
"service": "canadapost_regular_parcel",
|
|
966
|
-
"total_charge": 50.00,
|
|
967
|
-
"transit_days": 5,
|
|
968
|
-
"extra_charges": [
|
|
969
|
-
{"name": "Base charge", "amount": 50.00, "currency": "CAD", "id": None},
|
|
970
|
-
],
|
|
971
|
-
"meta": {
|
|
972
|
-
"service_name": "CANADAPOST REGULAR PARCEL",
|
|
973
|
-
"rate_provider": "canadapost",
|
|
974
|
-
"carrier_connection_id": ANY,
|
|
975
|
-
},
|
|
976
|
-
"test_mode": True,
|
|
1090
|
+
"weight": 1,
|
|
1091
|
+
"weight_unit": "KG",
|
|
1092
|
+
"package_preset": "canadapost_corrugated_small_box",
|
|
977
1093
|
}
|
|
978
1094
|
],
|
|
979
|
-
"
|
|
980
|
-
"label_type": "PDF",
|
|
981
|
-
"carrier_ids": [],
|
|
982
|
-
"tracker_id": ANY,
|
|
983
|
-
"created_at": ANY,
|
|
984
|
-
"metadata": {},
|
|
985
|
-
"messages": [],
|
|
986
|
-
"status": "purchased",
|
|
987
|
-
"carrier_name": "canadapost",
|
|
988
|
-
"carrier_id": "canadapost",
|
|
989
|
-
"tracking_number": "123456789012",
|
|
990
|
-
"shipment_identifier": "123456789012",
|
|
991
|
-
"selected_rate": {
|
|
992
|
-
"id": ANY,
|
|
993
|
-
"object_type": "rate",
|
|
994
|
-
"carrier_name": "canadapost",
|
|
995
|
-
"carrier_id": "canadapost",
|
|
996
|
-
"currency": "CAD",
|
|
997
|
-
"estimated_delivery": ANY,
|
|
998
|
-
"service": "canadapost_priority",
|
|
999
|
-
"total_charge": 50.00,
|
|
1000
|
-
"transit_days": 5,
|
|
1001
|
-
"extra_charges": [
|
|
1002
|
-
{"name": "Base charge", "amount": 50.00, "currency": "CAD", "id": None},
|
|
1003
|
-
],
|
|
1004
|
-
"meta": {
|
|
1005
|
-
"ext": "canadapost",
|
|
1006
|
-
"carrier": "canadapost",
|
|
1007
|
-
"service_name": "CANADAPOST REGULAR PARCEL",
|
|
1008
|
-
"rate_provider": "canadapost",
|
|
1009
|
-
"carrier_connection_id": ANY,
|
|
1010
|
-
"has_alternative_services": True,
|
|
1011
|
-
},
|
|
1012
|
-
"test_mode": True,
|
|
1013
|
-
},
|
|
1014
|
-
"meta": {
|
|
1015
|
-
"ext": "canadapost",
|
|
1016
|
-
"carrier": "canadapost",
|
|
1017
|
-
"rate_provider": "canadapost",
|
|
1018
|
-
"service_name": "CANADAPOST PRIORITY",
|
|
1019
|
-
},
|
|
1095
|
+
"payment": {"currency": "CAD", "paid_by": "sender"},
|
|
1020
1096
|
"service": "canadapost_priority",
|
|
1021
|
-
"
|
|
1022
|
-
"test_mode": True,
|
|
1023
|
-
"label_url": ANY,
|
|
1024
|
-
"invoice_url": None,
|
|
1097
|
+
"options": {"has_alternative_services": True},
|
|
1025
1098
|
}
|
{karrio_server_manager-2025.5.1.dist-info → karrio_server_manager-2025.5.2.dist-info}/RECORD
RENAMED
|
@@ -82,7 +82,7 @@ karrio/server/manager/serializers/manifest.py,sha256=mSneCk_7HMXpi64_7hggWvkR7Ma
|
|
|
82
82
|
karrio/server/manager/serializers/parcel.py,sha256=733Bg26lVbEkoWtAVM5Qt2IRBS2QDuVxhG40Hiqh3bw,2621
|
|
83
83
|
karrio/server/manager/serializers/pickup.py,sha256=sX0VmcQxGkXn3IEosMuFwdXh4HhdkPcuBOp79O8PoDQ,9233
|
|
84
84
|
karrio/server/manager/serializers/rate.py,sha256=7vYK_v8iWEDnswqYHG2Lir16_UhHTOxW5rdC6lw3lzA,652
|
|
85
|
-
karrio/server/manager/serializers/shipment.py,sha256=
|
|
85
|
+
karrio/server/manager/serializers/shipment.py,sha256=N4mld4eIM1HQ6NdsQ7gDt73aDv4j0wHM4AAMfgChnMc,34919
|
|
86
86
|
karrio/server/manager/serializers/tracking.py,sha256=ixrAjIiZQsvSt4y0qtisGkt6TFOJ3ORNkJAQVt6YQrA,12483
|
|
87
87
|
karrio/server/manager/tests/__init__.py,sha256=Y1UNteEE60vWdUAkjbldu_r_-h4u0He8-UoiBgTjKcU,391
|
|
88
88
|
karrio/server/manager/tests/test_addresses.py,sha256=pNkZC_yJyb29ZlEOtOAs4blcEYiOarw0zhZIZC5uj1w,3111
|
|
@@ -90,7 +90,7 @@ karrio/server/manager/tests/test_custom_infos.py,sha256=iv2cLdZVoVWFZK_mDUEnrZss
|
|
|
90
90
|
karrio/server/manager/tests/test_errors.py,sha256=QYsGLUtwMvrHeX1XSCpdteTKbug7-y1-Xgvbl96aN9g,3220
|
|
91
91
|
karrio/server/manager/tests/test_parcels.py,sha256=lVLBOsHzXgXQvYjHIUy5oiPvrMfxYpueVvvhtuhstWk,2559
|
|
92
92
|
karrio/server/manager/tests/test_pickups.py,sha256=8jxddwTnBvBM9FOyWxW9TtZ-GOVYUje7HQ2EZjsbtD8,10681
|
|
93
|
-
karrio/server/manager/tests/test_shipments.py,sha256=
|
|
93
|
+
karrio/server/manager/tests/test_shipments.py,sha256=LBblskbeJyUvWtJdy5hZPvumuOT2PE__ikIK3YlvcnY,35914
|
|
94
94
|
karrio/server/manager/tests/test_trackers.py,sha256=KvmWkplokNDZ0dzB16mFl0WcMJ0OYp_ErZeWJPGW_NA,7151
|
|
95
95
|
karrio/server/manager/views/__init__.py,sha256=kDFUaORRQ3Xh0ZPm-Jk88Ss8dgGYM57iUFXb9TPMzh0,401
|
|
96
96
|
karrio/server/manager/views/addresses.py,sha256=7YCAs2ZYgd1icYwMcGGWfX7A7vZEL4BEAbU4eIxhiMY,4620
|
|
@@ -101,7 +101,7 @@ karrio/server/manager/views/parcels.py,sha256=hZY45rg6SrTWfQqyJ38MGKSor1yqgPUEVH
|
|
|
101
101
|
karrio/server/manager/views/pickups.py,sha256=gmpxz9ot1OR-BP1qh-0MXU3kUJi1ht_74hfaLJzJ42w,5503
|
|
102
102
|
karrio/server/manager/views/shipments.py,sha256=TqLpBH5Jf-rI3enJwvNptRwGzfo7co9R1VSP_oqhB3o,10419
|
|
103
103
|
karrio/server/manager/views/trackers.py,sha256=3oGn2qDpHgk8GZvuz-Cb93Fc0j_h_HbXQR692Zhfiok,12363
|
|
104
|
-
karrio_server_manager-2025.5.
|
|
105
|
-
karrio_server_manager-2025.5.
|
|
106
|
-
karrio_server_manager-2025.5.
|
|
107
|
-
karrio_server_manager-2025.5.
|
|
104
|
+
karrio_server_manager-2025.5.2.dist-info/METADATA,sha256=WZxA5Br3FwwK7PO0Au-FKvQPHFPUcXevyuoaK596rWQ,730
|
|
105
|
+
karrio_server_manager-2025.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
106
|
+
karrio_server_manager-2025.5.2.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
|
|
107
|
+
karrio_server_manager-2025.5.2.dist-info/RECORD,,
|
|
File without changes
|
{karrio_server_manager-2025.5.1.dist-info → karrio_server_manager-2025.5.2.dist-info}/top_level.txt
RENAMED
|
File without changes
|