karrio-server-graph 2025.5rc11__py3-none-any.whl → 2025.5rc13__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.
@@ -332,6 +332,24 @@ class Mutation:
332
332
  ) -> mutations.UpdateRateSheetMutation:
333
333
  return mutations.UpdateRateSheetMutation.mutate(info, **input.to_dict())
334
334
 
335
+ @strawberry.mutation
336
+ def update_rate_sheet_zone_cell(
337
+ self, info: Info, input: inputs.UpdateRateSheetZoneCellMutationInput
338
+ ) -> mutations.UpdateRateSheetZoneCellMutation:
339
+ return mutations.UpdateRateSheetZoneCellMutation.mutate(info, **input.to_dict())
340
+
341
+ @strawberry.mutation
342
+ def batch_update_rate_sheet_cells(
343
+ self, info: Info, input: inputs.BatchUpdateRateSheetCellsMutationInput
344
+ ) -> mutations.BatchUpdateRateSheetCellsMutation:
345
+ return mutations.BatchUpdateRateSheetCellsMutation.mutate(info, **input.to_dict())
346
+
347
+ @strawberry.mutation
348
+ def delete_rate_sheet_service(
349
+ self, info: Info, input: inputs.DeleteRateSheetServiceMutationInput
350
+ ) -> mutations.DeleteRateSheetServiceMutation:
351
+ return mutations.DeleteRateSheetServiceMutation.mutate(info, **input.to_dict())
352
+
335
353
  @strawberry.mutation
336
354
  def delete_rate_sheet(
337
355
  self, info: Info, input: inputs.DeleteMutationInput
@@ -245,7 +245,6 @@ class AddressInput:
245
245
  email: typing.Optional[str] = strawberry.UNSET
246
246
  phone_number: typing.Optional[str] = strawberry.UNSET
247
247
  state_code: typing.Optional[str] = strawberry.UNSET
248
- suburb: typing.Optional[str] = strawberry.UNSET
249
248
  residential: typing.Optional[bool] = strawberry.UNSET
250
249
  street_number: typing.Optional[str] = strawberry.UNSET
251
250
  address_line1: typing.Optional[str] = strawberry.UNSET
@@ -517,6 +516,35 @@ class UpdateRateSheetMutationInput(utils.BaseInput):
517
516
  metadata: typing.Optional[utils.JSON] = strawberry.UNSET
518
517
 
519
518
 
519
+ @strawberry.input
520
+ class UpdateRateSheetZoneCellMutationInput(utils.BaseInput):
521
+ id: str # Rate sheet ID
522
+ service_id: str # Service level ID
523
+ zone_id: str # Zone ID
524
+ field: str # Field name to update
525
+ value: utils.JSON # New value
526
+
527
+
528
+ @strawberry.input
529
+ class CellUpdate(utils.BaseInput):
530
+ service_id: str
531
+ zone_id: str
532
+ field: str
533
+ value: utils.JSON
534
+
535
+
536
+ @strawberry.input
537
+ class BatchUpdateRateSheetCellsMutationInput(utils.BaseInput):
538
+ id: str # Rate sheet ID
539
+ updates: typing.List[CellUpdate]
540
+
541
+
542
+ @strawberry.input
543
+ class DeleteRateSheetServiceMutationInput(utils.BaseInput):
544
+ rate_sheet_id: str
545
+ service_id: str
546
+
547
+
520
548
  @strawberry.input
521
549
  class RateSheetFilter(utils.Paginated):
522
550
  keyword: typing.Optional[str] = strawberry.UNSET
@@ -538,8 +538,8 @@ class CreateRateSheetMutation(utils.BaseMutation):
538
538
  carrier_name=rate_sheet.carrier_name,
539
539
  ).filter(id__in=carriers)
540
540
  for _ in _carriers:
541
- _.settings.rate_sheet = rate_sheet
542
- _.settings.save(update_fields=["rate_sheet"])
541
+ _.rate_sheet = rate_sheet
542
+ _.save(update_fields=["rate_sheet"])
543
543
 
544
544
  return CreateRateSheetMutation(rate_sheet=rate_sheet)
545
545
 
@@ -558,9 +558,10 @@ class UpdateRateSheetMutation(utils.BaseMutation):
558
558
  instance = providers.RateSheet.access_by(info.context.request).get(
559
559
  id=input["id"]
560
560
  )
561
+ data = input.copy()
561
562
  serializer = serializers.RateSheetModelSerializer(
562
563
  instance,
563
- data=input,
564
+ data=data,
564
565
  context=info.context.request,
565
566
  partial=True,
566
567
  )
@@ -568,11 +569,136 @@ class UpdateRateSheetMutation(utils.BaseMutation):
568
569
  serializer.is_valid(raise_exception=True)
569
570
  rate_sheet = serializer.save()
570
571
 
572
+ # Handle services updates like in CreateRateSheetMutation
573
+ if "services" in data:
574
+ save_many_to_many_data(
575
+ "services",
576
+ serializers.ServiceLevelModelSerializer,
577
+ rate_sheet,
578
+ payload=data,
579
+ context=info.context.request,
580
+ )
581
+
571
582
  return UpdateRateSheetMutation(
572
583
  rate_sheet=providers.RateSheet.objects.get(id=input["id"])
573
584
  )
574
585
 
575
586
 
587
+ @strawberry.type
588
+ class UpdateRateSheetZoneCellMutation(utils.BaseMutation):
589
+ rate_sheet: typing.Optional[types.RateSheetType] = None
590
+
591
+ @staticmethod
592
+ @transaction.atomic
593
+ @utils.authentication_required
594
+ @utils.authorization_required(["manage_carriers"])
595
+ def mutate(
596
+ info: Info, **input: inputs.UpdateRateSheetZoneCellMutationInput
597
+ ) -> "UpdateRateSheetZoneCellMutation":
598
+ rate_sheet = providers.RateSheet.access_by(info.context.request).get(
599
+ id=input["id"]
600
+ )
601
+ service = rate_sheet.services.get(id=input["service_id"])
602
+
603
+ try:
604
+ service.update_zone_cell(
605
+ zone_id=input["zone_id"], field=input["field"], value=input["value"]
606
+ )
607
+ except ValueError as e:
608
+ logger.error(f"Invalid zone id: {e}")
609
+ raise exceptions.ValidationError({"zone_id": "invalid zone id"})
610
+
611
+ return UpdateRateSheetZoneCellMutation(rate_sheet=rate_sheet)
612
+
613
+
614
+ @strawberry.type
615
+ class BatchUpdateRateSheetCellsMutation(utils.BaseMutation):
616
+ rate_sheet: typing.Optional[types.RateSheetType] = None
617
+
618
+ @staticmethod
619
+ @transaction.atomic
620
+ @utils.authentication_required
621
+ @utils.authorization_required(["manage_carriers"])
622
+ def mutate(
623
+ info: Info, **input: inputs.BatchUpdateRateSheetCellsMutationInput
624
+ ) -> "BatchUpdateRateSheetCellsMutation":
625
+ rate_sheet = providers.RateSheet.access_by(info.context.request).get(
626
+ id=input["id"]
627
+ )
628
+
629
+ # Group updates by service_id for efficient processing
630
+ service_updates = {}
631
+ for update in input["updates"]:
632
+ service_id = update["service_id"]
633
+ if service_id not in service_updates:
634
+ service_updates[service_id] = []
635
+ service_updates[service_id].append(
636
+ {
637
+ "zone_id": update["zone_id"],
638
+ "field": update["field"],
639
+ "value": update["value"],
640
+ }
641
+ )
642
+
643
+ # Use optimized structure if available, otherwise fall back to legacy
644
+ if rate_sheet.zones is not None and rate_sheet.service_rates is not None:
645
+ # Use optimized batch update on rate sheet
646
+ all_updates = []
647
+ for service_id, updates in service_updates.items():
648
+ for update in updates:
649
+ all_updates.append(
650
+ {
651
+ "service_id": service_id,
652
+ "zone_id": update["zone_id"],
653
+ "field": update["field"],
654
+ "value": update["value"],
655
+ }
656
+ )
657
+ try:
658
+ rate_sheet.batch_update_service_rates(all_updates)
659
+ except Exception as e:
660
+ logger.error(f"Invalid zone id: {e}")
661
+ raise exceptions.ValidationError(
662
+ {"rate_sheet": "failed to update rate sheet"}
663
+ )
664
+ else:
665
+ # Fall back to legacy per-service updates
666
+ for service_id, updates in service_updates.items():
667
+ try:
668
+ service = rate_sheet.services.get(id=service_id)
669
+ service.batch_update_cells(updates)
670
+ except ValueError as e:
671
+ logger.error(f"Invalid zone id: {e}")
672
+ raise exceptions.ValidationError(
673
+ {"service_id": "failed to update service"}
674
+ )
675
+
676
+ return BatchUpdateRateSheetCellsMutation(rate_sheet=rate_sheet)
677
+
678
+
679
+ @strawberry.type
680
+ class DeleteRateSheetServiceMutation(utils.BaseMutation):
681
+ rate_sheet: typing.Optional[types.RateSheetType] = None
682
+
683
+ @staticmethod
684
+ @transaction.atomic
685
+ @utils.authentication_required
686
+ @utils.authorization_required(["manage_carriers"])
687
+ def mutate(
688
+ info: Info, **input: inputs.DeleteRateSheetServiceMutationInput
689
+ ) -> "DeleteRateSheetServiceMutation":
690
+ rate_sheet = providers.RateSheet.access_by(info.context.request).get(
691
+ id=input["rate_sheet_id"]
692
+ )
693
+ service = rate_sheet.services.get(id=input["service_id"])
694
+
695
+ # Remove service from rate sheet and delete it
696
+ rate_sheet.services.remove(service)
697
+ service.delete()
698
+
699
+ return DeleteRateSheetServiceMutation(rate_sheet=rate_sheet)
700
+
701
+
576
702
  @strawberry.type
577
703
  class PartialShipmentMutation(utils.BaseMutation):
578
704
  shipment: typing.Optional[types.ShipmentType] = None
@@ -841,10 +967,14 @@ class CreateMetafieldMutation(utils.BaseMutation):
841
967
  ) -> "CreateMetafieldMutation":
842
968
  data = input.copy()
843
969
 
844
- metafield = serializers.MetafieldModelSerializer.map(
845
- data=data,
846
- context=info.context.request,
847
- ).save().instance
970
+ metafield = (
971
+ serializers.MetafieldModelSerializer.map(
972
+ data=data,
973
+ context=info.context.request,
974
+ )
975
+ .save()
976
+ .instance
977
+ )
848
978
 
849
979
  return CreateMetafieldMutation(metafield=metafield)
850
980
 
@@ -862,10 +992,14 @@ class UpdateMetafieldMutation(utils.BaseMutation):
862
992
  data = input.copy()
863
993
  instance = core.Metafield.access_by(info.context.request).get(id=data.get("id"))
864
994
 
865
- metafield = serializers.MetafieldModelSerializer.map(
866
- instance,
867
- data=data,
868
- context=info.context.request,
869
- ).save().instance
995
+ metafield = (
996
+ serializers.MetafieldModelSerializer.map(
997
+ instance,
998
+ data=data,
999
+ context=info.context.request,
1000
+ )
1001
+ .save()
1002
+ .instance
1003
+ )
870
1004
 
871
1005
  return UpdateMetafieldMutation(metafield=metafield)
@@ -639,7 +639,6 @@ class AddressType:
639
639
  email: typing.Optional[str]
640
640
  phone_number: typing.Optional[str]
641
641
  state_code: typing.Optional[str]
642
- suburb: typing.Optional[str]
643
642
  residential: typing.Optional[bool]
644
643
  street_number: typing.Optional[str]
645
644
  address_line1: typing.Optional[str]
@@ -1118,6 +1117,7 @@ class ShipmentType:
1118
1117
  @strawberry.type
1119
1118
  class ServiceZoneType:
1120
1119
  object_type: str
1120
+ id: typing.Optional[str] = None
1121
1121
  label: typing.Optional[str] = None
1122
1122
  rate: typing.Optional[float] = None
1123
1123
 
@@ -1176,7 +1176,8 @@ class ServiceLevelType:
1176
1176
 
1177
1177
  @strawberry.field
1178
1178
  def zones(self: providers.ServiceLevel) -> typing.List[ServiceZoneType]:
1179
- return [ServiceZoneType.parse(zone) for zone in self.zones or []]
1179
+ # Use computed_zones for backward compatibility with optimized structure
1180
+ return [ServiceZoneType.parse(zone) for zone in self.computed_zones]
1180
1181
 
1181
1182
  @strawberry.field
1182
1183
  def metadata(self: providers.RateSheet) -> typing.Optional[utils.JSON]:
@@ -12,6 +12,7 @@ import karrio.server.manager.models as manager
12
12
  import karrio.server.graph.models as graph
13
13
  import karrio.server.core.models as core
14
14
  import karrio.server.user.models as auth
15
+ import karrio.server.core.gateway as gateway
15
16
 
16
17
 
17
18
  class UserModelSerializer(serializers.ModelSerializer):
@@ -268,10 +269,10 @@ class ServiceLevelModelSerializer(serializers.ModelSerializer):
268
269
  )
269
270
  self.instance.save(update_fields=["zones"])
270
271
 
271
- def update(self, instance, validated_data):
272
+ def update(self, instance, validated_data, context=None, **kwargs):
272
273
  """Handle partial updates of service level data including zones."""
273
274
  zones_data = validated_data.pop("zones", None)
274
- instance = super().update(instance, validated_data)
275
+ instance = super().update(instance, validated_data, context=context)
275
276
 
276
277
  if zones_data is not None:
277
278
  # Handle zone updates if provided
@@ -362,10 +363,10 @@ class RateSheetModelSerializer(serializers.ModelSerializer):
362
363
  ).filter(id__in=list(_ids))
363
364
 
364
365
  for carrier in _carriers:
365
- carrier.settings.rate_sheet = (
366
+ carrier.rate_sheet = (
366
367
  self.instance if carrier.id in carriers else None
367
368
  )
368
- carrier.settings.save(update_fields=["rate_sheet"])
369
+ carrier.save(update_fields=["rate_sheet"])
369
370
 
370
371
  def update(self, instance, validated_data, **kwargs):
371
372
  """Handle updates of rate sheet data including services and carriers."""
@@ -0,0 +1,603 @@
1
+ import json
2
+ from unittest.mock import ANY, patch
3
+ from django.urls import reverse
4
+ from rest_framework import status
5
+ from karrio.core.models import RateDetails, ChargeDetails
6
+ from karrio.server.graph.tests.base import GraphTestCase, Result
7
+ import karrio.server.manager.models as manager
8
+
9
+
10
+ class TestPartialShipmentMutation(GraphTestCase):
11
+ def setUp(self) -> None:
12
+ super().setUp()
13
+ # Create a draft shipment using the REST API
14
+ self.shipment = self._create_draft_shipment()
15
+
16
+ def _create_draft_shipment(self):
17
+ """Create a draft shipment using the REST API without service (no rates fetched)"""
18
+ url = reverse("karrio.server.manager:shipment-list")
19
+
20
+ with patch("karrio.server.core.gateway.utils.identity") as mock:
21
+ mock.return_value = RETURNED_RATES_VALUE
22
+ response = self.client.post(url, DRAFT_SHIPMENT_DATA)
23
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
24
+ response_data = json.loads(response.content)
25
+ return response_data
26
+
27
+ def test_partial_update_options_with_none_values(self):
28
+ """Test updating options with explicit None values to remove existing options"""
29
+ shipment_id = self.shipment["id"]
30
+
31
+ response = self.query(
32
+ """
33
+ mutation partial_shipment_update($data: PartialShipmentMutationInput!) {
34
+ partial_shipment_update(input: $data) {
35
+ shipment {
36
+ id
37
+ options
38
+ }
39
+ errors {
40
+ field
41
+ messages
42
+ }
43
+ }
44
+ }
45
+ """,
46
+ operation_name="partial_shipment_update",
47
+ variables={
48
+ "data": {
49
+ "id": shipment_id,
50
+ "options": {
51
+ "insurance": None,
52
+ "signature_confirmation": None,
53
+ "currency": "USD",
54
+ "new_option": "test_value",
55
+ },
56
+ }
57
+ },
58
+ )
59
+
60
+ self.assertResponseNoErrors(response)
61
+ self.assertDictEqual(response.data, OPTIONS_UPDATE_RESPONSE)
62
+
63
+ def test_partial_update_shipper_address_fields(self):
64
+ """Test updating random fields within shipper address"""
65
+ shipment_id = self.shipment["id"]
66
+
67
+ response = self.query(
68
+ """
69
+ mutation partial_shipment_update($data: PartialShipmentMutationInput!) {
70
+ partial_shipment_update(input: $data) {
71
+ shipment {
72
+ id
73
+ shipper {
74
+ id
75
+ company_name
76
+ person_name
77
+ phone_number
78
+ email
79
+ city
80
+ postal_code
81
+ }
82
+ }
83
+ errors {
84
+ field
85
+ messages
86
+ }
87
+ }
88
+ }
89
+ """,
90
+ operation_name="partial_shipment_update",
91
+ variables={
92
+ "data": {
93
+ "id": shipment_id,
94
+ "shipper": {
95
+ "company_name": "Updated Corp Inc.",
96
+ "email": "updated@example.com",
97
+ "phone_number": "555-123-4567",
98
+ },
99
+ }
100
+ },
101
+ )
102
+
103
+ self.assertResponseNoErrors(response)
104
+ self.assertDictEqual(response.data, SHIPPER_UPDATE_RESPONSE)
105
+
106
+ def test_partial_update_recipient_address_fields(self):
107
+ """Test updating random fields within recipient address"""
108
+ shipment_id = self.shipment["id"]
109
+
110
+ response = self.query(
111
+ """
112
+ mutation partial_shipment_update($data: PartialShipmentMutationInput!) {
113
+ partial_shipment_update(input: $data) {
114
+ shipment {
115
+ id
116
+ recipient {
117
+ id
118
+ company_name
119
+ person_name
120
+ address_line1
121
+ city
122
+ state_code
123
+ residential
124
+ }
125
+ }
126
+ errors {
127
+ field
128
+ messages
129
+ }
130
+ }
131
+ }
132
+ """,
133
+ operation_name="partial_shipment_update",
134
+ variables={
135
+ "data": {
136
+ "id": shipment_id,
137
+ "recipient": {
138
+ "person_name": "John Updated",
139
+ "address_line1": "456 Updated St",
140
+ "residential": True,
141
+ },
142
+ }
143
+ },
144
+ )
145
+
146
+ self.assertResponseNoErrors(response)
147
+ self.assertDictEqual(response.data, RECIPIENT_UPDATE_RESPONSE)
148
+
149
+ def test_partial_update_payment_and_metadata(self):
150
+ """Test updating payment information and metadata"""
151
+ shipment_id = self.shipment["id"]
152
+
153
+ response = self.query(
154
+ """
155
+ mutation partial_shipment_update($data: PartialShipmentMutationInput!) {
156
+ partial_shipment_update(input: $data) {
157
+ shipment {
158
+ id
159
+ payment {
160
+ paid_by
161
+ currency
162
+ account_number
163
+ }
164
+ metadata
165
+ reference
166
+ }
167
+ errors {
168
+ field
169
+ messages
170
+ }
171
+ }
172
+ }
173
+ """,
174
+ operation_name="partial_shipment_update",
175
+ variables={
176
+ "data": {
177
+ "id": shipment_id,
178
+ "payment": {"paid_by": "recipient", "account_number": "123456789"},
179
+ "metadata": {"customer_id": "CUST123", "order_number": "ORD456"},
180
+ "reference": "REF789",
181
+ }
182
+ },
183
+ )
184
+
185
+ self.assertResponseNoErrors(response)
186
+ self.assertDictEqual(response.data, PAYMENT_METADATA_UPDATE_RESPONSE)
187
+
188
+ def test_partial_update_parcel_information(self):
189
+ """Test updating parcel information"""
190
+ shipment_id = self.shipment["id"]
191
+
192
+ response = self.query(
193
+ """
194
+ mutation partial_shipment_update($data: PartialShipmentMutationInput!) {
195
+ partial_shipment_update(input: $data) {
196
+ shipment {
197
+ id
198
+ parcels {
199
+ id
200
+ weight
201
+ weight_unit
202
+ package_preset
203
+ description
204
+ reference_number
205
+ }
206
+ }
207
+ errors {
208
+ field
209
+ messages
210
+ }
211
+ }
212
+ }
213
+ """,
214
+ operation_name="partial_shipment_update",
215
+ variables={
216
+ "data": {
217
+ "id": shipment_id,
218
+ "parcels": [
219
+ {
220
+ "weight": 2.5,
221
+ "weight_unit": "LB",
222
+ "description": "Updated parcel description",
223
+ "package_preset": "canadapost_corrugated_medium_box",
224
+ }
225
+ ],
226
+ }
227
+ },
228
+ )
229
+
230
+ self.assertResponseNoErrors(response)
231
+ self.assertDictEqual(response.data, PARCEL_UPDATE_RESPONSE)
232
+
233
+ def test_partial_update_label_type_and_options_merge(self):
234
+ """Test updating label type and ensuring options are properly merged"""
235
+ shipment_id = self.shipment["id"]
236
+
237
+ # First, set some initial options
238
+ self.query(
239
+ """
240
+ mutation partial_shipment_update($data: PartialShipmentMutationInput!) {
241
+ partial_shipment_update(input: $data) {
242
+ shipment {
243
+ id
244
+ options
245
+ }
246
+ }
247
+ }
248
+ """,
249
+ operation_name="partial_shipment_update",
250
+ variables={
251
+ "data": {
252
+ "id": shipment_id,
253
+ "options": {
254
+ "insurance": 100,
255
+ "signature_confirmation": True,
256
+ "currency": "CAD",
257
+ },
258
+ }
259
+ },
260
+ )
261
+
262
+ # Now update label type and add more options
263
+ response = self.query(
264
+ """
265
+ mutation partial_shipment_update($data: PartialShipmentMutationInput!) {
266
+ partial_shipment_update(input: $data) {
267
+ shipment {
268
+ id
269
+ label_type
270
+ options
271
+ }
272
+ errors {
273
+ field
274
+ messages
275
+ }
276
+ }
277
+ }
278
+ """,
279
+ operation_name="partial_shipment_update",
280
+ variables={
281
+ "data": {
282
+ "id": shipment_id,
283
+ "label_type": "ZPL",
284
+ "options": {"delivery_confirmation": True, "priority": "high"},
285
+ }
286
+ },
287
+ )
288
+
289
+ self.assertResponseNoErrors(response)
290
+ self.assertDictEqual(response.data, LABEL_OPTIONS_MERGE_RESPONSE)
291
+
292
+ def test_full_shipment_update_simulation(self):
293
+ """Test updating multiple fields at once to simulate frontend save operation"""
294
+ shipment_id = self.shipment["id"]
295
+
296
+ # Set the shipment ID dynamically in the test data
297
+ full_update_data = FULL_UPDATE_DATA.copy()
298
+ full_update_data["data"]["id"] = shipment_id
299
+
300
+ response = self.query(
301
+ """
302
+ mutation partial_shipment_update($data: PartialShipmentMutationInput!) {
303
+ partial_shipment_update(input: $data) {
304
+ shipment {
305
+ id
306
+ status
307
+ shipper {
308
+ company_name
309
+ person_name
310
+ email
311
+ }
312
+ recipient {
313
+ company_name
314
+ person_name
315
+ email
316
+ }
317
+ parcels {
318
+ weight
319
+ weight_unit
320
+ description
321
+ }
322
+ payment {
323
+ paid_by
324
+ currency
325
+ }
326
+ options
327
+ metadata
328
+ reference
329
+ label_type
330
+ }
331
+ errors {
332
+ field
333
+ messages
334
+ }
335
+ }
336
+ }
337
+ """,
338
+ operation_name="partial_shipment_update",
339
+ variables=full_update_data,
340
+ )
341
+
342
+ self.assertResponseNoErrors(response)
343
+ self.assertDictEqual(response.data, FULL_UPDATE_SIMULATION_RESPONSE)
344
+
345
+
346
+ # Test data
347
+ DRAFT_SHIPMENT_DATA = {
348
+ "recipient": {
349
+ "address_line1": "125 Church St",
350
+ "person_name": "John Poop",
351
+ "company_name": "A corp.",
352
+ "phone_number": "514 000 0000",
353
+ "city": "Moncton",
354
+ "country_code": "CA",
355
+ "postal_code": "E1C4Z8",
356
+ "residential": False,
357
+ "state_code": "NB",
358
+ },
359
+ "shipper": {
360
+ "address_line1": "5840 Oak St",
361
+ "person_name": "Jane Doe",
362
+ "company_name": "B corp.",
363
+ "phone_number": "514 000 9999",
364
+ "city": "Vancouver",
365
+ "country_code": "CA",
366
+ "postal_code": "V6M2V9",
367
+ "residential": False,
368
+ "state_code": "BC",
369
+ },
370
+ "parcels": [
371
+ {
372
+ "weight": 1,
373
+ "weight_unit": "KG",
374
+ "package_preset": "canadapost_corrugated_small_box",
375
+ }
376
+ ],
377
+ "payment": {"currency": "CAD", "paid_by": "sender"},
378
+ # Note: No service specified, so this creates a draft without rates
379
+ }
380
+
381
+ RETURNED_RATES_VALUE = [
382
+ [
383
+ RateDetails(
384
+ carrier_id="canadapost",
385
+ carrier_name="canadapost",
386
+ currency="CAD",
387
+ transit_days=2,
388
+ service="canadapost_priority",
389
+ total_charge=106.71,
390
+ extra_charges=[
391
+ ChargeDetails(amount=13.92, currency="CAD", name="Duty and taxes"),
392
+ ChargeDetails(amount=2.7, currency="CAD", name="Fuel surcharge"),
393
+ ChargeDetails(amount=-11.74, currency="CAD", name="SMB Savings"),
394
+ ChargeDetails(amount=-9.04, currency="CAD", name="Discount"),
395
+ ChargeDetails(amount=101.83, currency="CAD", name="Base surcharge"),
396
+ ],
397
+ )
398
+ ],
399
+ [],
400
+ ]
401
+
402
+ FULL_UPDATE_DATA = {
403
+ "data": {
404
+ "id": None, # Will be set dynamically in the test
405
+ "shipper": {
406
+ "company_name": "Full Update Shipper Corp",
407
+ "person_name": "Updated Shipper",
408
+ "email": "shipper@fullupdate.com",
409
+ },
410
+ "recipient": {
411
+ "company_name": "Full Update Recipient Corp",
412
+ "person_name": "Updated Recipient",
413
+ "email": "recipient@fullupdate.com",
414
+ },
415
+ "parcels": [
416
+ {"weight": 3.0, "weight_unit": "KG", "description": "Full update parcel"}
417
+ ],
418
+ "payment": {"paid_by": "third_party", "currency": "USD"},
419
+ "options": {"insurance": 250, "priority": "express"},
420
+ "metadata": {"full_update": True, "test_case": "full_update_simulation"},
421
+ "reference": "FULL_UPDATE_REF",
422
+ "label_type": "PNG",
423
+ }
424
+ }
425
+
426
+ # Expected responses for assertDictEqual
427
+ OPTIONS_UPDATE_RESPONSE = {
428
+ "data": {
429
+ "partial_shipment_update": {
430
+ "shipment": {
431
+ "id": ANY,
432
+ "options": {
433
+ "shipping_date": ANY,
434
+ "shipment_date": ANY,
435
+ "currency": "USD",
436
+ "new_option": "test_value",
437
+ },
438
+ },
439
+ "errors": None,
440
+ }
441
+ }
442
+ }
443
+
444
+ SHIPPER_UPDATE_RESPONSE = {
445
+ "data": {
446
+ "partial_shipment_update": {
447
+ "shipment": {
448
+ "id": ANY,
449
+ "shipper": {
450
+ "id": ANY,
451
+ "company_name": "Updated Corp Inc.",
452
+ "person_name": "Jane Doe",
453
+ "phone_number": "555-123-4567",
454
+ "email": "updated@example.com",
455
+ "city": "Vancouver",
456
+ "postal_code": "V6M2V9",
457
+ },
458
+ },
459
+ "errors": None,
460
+ }
461
+ }
462
+ }
463
+
464
+ RECIPIENT_UPDATE_RESPONSE = {
465
+ "data": {
466
+ "partial_shipment_update": {
467
+ "shipment": {
468
+ "id": ANY,
469
+ "recipient": {
470
+ "id": ANY,
471
+ "company_name": "A corp.",
472
+ "person_name": "John Updated",
473
+ "address_line1": "456 Updated St",
474
+ "city": "Moncton",
475
+ "state_code": "NB",
476
+ "residential": True,
477
+ },
478
+ },
479
+ "errors": None,
480
+ }
481
+ }
482
+ }
483
+
484
+ PAYMENT_METADATA_UPDATE_RESPONSE = {
485
+ "data": {
486
+ "partial_shipment_update": {
487
+ "shipment": {
488
+ "id": ANY,
489
+ "payment": {
490
+ "paid_by": "recipient",
491
+ "currency": None,
492
+ "account_number": "123456789",
493
+ },
494
+ "metadata": {
495
+ "customer_id": "CUST123",
496
+ "order_number": "ORD456",
497
+ },
498
+ "reference": "REF789",
499
+ },
500
+ "errors": None,
501
+ }
502
+ }
503
+ }
504
+
505
+ PARCEL_UPDATE_RESPONSE = {
506
+ "data": {
507
+ "partial_shipment_update": {
508
+ "shipment": {
509
+ "id": ANY,
510
+ "parcels": [
511
+ {
512
+ "id": ANY,
513
+ "weight": 1.0,
514
+ "weight_unit": "KG",
515
+ "package_preset": "canadapost_corrugated_small_box",
516
+ "description": None,
517
+ "reference_number": ANY,
518
+ },
519
+ {
520
+ "id": ANY,
521
+ "weight": 2.5,
522
+ "weight_unit": "LB",
523
+ "package_preset": "canadapost_corrugated_medium_box",
524
+ "description": "Updated parcel description",
525
+ "reference_number": ANY,
526
+ },
527
+ ],
528
+ },
529
+ "errors": None,
530
+ }
531
+ }
532
+ }
533
+
534
+ LABEL_OPTIONS_MERGE_RESPONSE = {
535
+ "data": {
536
+ "partial_shipment_update": {
537
+ "shipment": {
538
+ "id": ANY,
539
+ "label_type": "ZPL",
540
+ "options": {
541
+ "shipping_date": ANY,
542
+ "shipment_date": ANY,
543
+ "currency": "CAD",
544
+ "insurance": 100,
545
+ "signature_confirmation": True,
546
+ "delivery_confirmation": True,
547
+ "priority": "high",
548
+ },
549
+ },
550
+ "errors": None,
551
+ }
552
+ }
553
+ }
554
+
555
+ FULL_UPDATE_SIMULATION_RESPONSE = {
556
+ "data": {
557
+ "partial_shipment_update": {
558
+ "shipment": {
559
+ "id": ANY,
560
+ "status": "draft",
561
+ "shipper": {
562
+ "company_name": "Full Update Shipper Corp",
563
+ "person_name": "Updated Shipper",
564
+ "email": "shipper@fullupdate.com",
565
+ },
566
+ "recipient": {
567
+ "company_name": "Full Update Recipient Corp",
568
+ "person_name": "Updated Recipient",
569
+ "email": "recipient@fullupdate.com",
570
+ },
571
+ "parcels": [
572
+ {
573
+ "weight": 1.0,
574
+ "weight_unit": "KG",
575
+ "description": None,
576
+ },
577
+ {
578
+ "weight": 3.0,
579
+ "weight_unit": "KG",
580
+ "description": "Full update parcel",
581
+ },
582
+ ],
583
+ "payment": {
584
+ "paid_by": "third_party",
585
+ "currency": "USD",
586
+ },
587
+ "options": {
588
+ "shipping_date": ANY,
589
+ "shipment_date": ANY,
590
+ "insurance": 250,
591
+ "priority": "express",
592
+ },
593
+ "metadata": {
594
+ "full_update": True,
595
+ "test_case": "full_update_simulation",
596
+ },
597
+ "reference": "FULL_UPDATE_REF",
598
+ "label_type": "PNG",
599
+ },
600
+ "errors": None,
601
+ }
602
+ }
603
+ }
@@ -305,8 +305,14 @@ UPDATE_RATE_SHEET_RESPONSE = {
305
305
  "services": [
306
306
  {
307
307
  "id": ANY,
308
- "service_name": "UPS Standard",
309
- "zones": [{"label": "Zone 1", "rate": 10.0}],
308
+ "service_name": "Updated Service",
309
+ "zones": [
310
+ {
311
+ "country_codes": ["US", "CA"],
312
+ "label": "Updated Zone",
313
+ "rate": 20.0,
314
+ }
315
+ ],
310
316
  }
311
317
  ],
312
318
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karrio_server_graph
3
- Version: 2025.5rc11
3
+ Version: 2025.5rc13
4
4
  Summary: Multi-carrier shipping API Graph module
5
5
  Author-email: karrio <hello@karrio.io>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ karrio/server/graph/apps.py,sha256=grL5ySDnW2amzj0nTXFm-WCoTWViVYvKqGlsi4k5epI,9
4
4
  karrio/server/graph/forms.py,sha256=5iGC3hj6dsAsupYz4QubyygTeTKDRR56Q5mYq0vbGhI,1962
5
5
  karrio/server/graph/models.py,sha256=CEnE4AsVyjBufyK6ebWmUH3s8DwA0HvZg0fUoZb5Pn4,1321
6
6
  karrio/server/graph/schema.py,sha256=2dXM8nD1usOc1S6QSalajoFmgwYuXxsrwj20AJ5HtT4,1151
7
- karrio/server/graph/serializers.py,sha256=vwP_U59aLnsnYVjwn3jrpz7SxOFzT_vplzSK-2FcsrU,13692
7
+ karrio/server/graph/serializers.py,sha256=kAVpyoN74gkA20ZnmDycvr_yrwFOb_A9di2-vrkdL7U,13760
8
8
  karrio/server/graph/urls.py,sha256=HKo0gkx5TgoWDV0ap2QCtueNTmaAqvX6qVDe3c2UT1E,183
9
9
  karrio/server/graph/utils.py,sha256=eeIKeTfGP-8U6MkdMdTyobnk6wbNEE75Bm0cFu85RfU,9031
10
10
  karrio/server/graph/views.py,sha256=qWfa-wteB-Mb7glAYz6SVlZSwHPw_ImMm7XVmewdmTQ,2889
@@ -15,10 +15,10 @@ karrio/server/graph/migrations/0001_initial.py,sha256=oeaS5JSkQP6w9ulYar3mdn695y
15
15
  karrio/server/graph/migrations/0002_auto_20210512_1353.py,sha256=TnUwR9EP0qp3gJ38f9w0PYawK2VheDtqXEgyRhYZS2M,538
16
16
  karrio/server/graph/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  karrio/server/graph/schemas/__init__.py,sha256=Kn-I1j3HP3jwZccpz6FL9r1k6b3UEAMVh2kFPCKNS0w,80
18
- karrio/server/graph/schemas/base/__init__.py,sha256=aarmPw7csVZCeRxRaU6FNZ6tbN8SOla-nbYqlXFn3lw,14272
19
- karrio/server/graph/schemas/base/inputs.py,sha256=Lptv7JfyXP6EKMSQ838oZ4Qyr9GckbrJzXiLg1CCJMc,20762
20
- karrio/server/graph/schemas/base/mutations.py,sha256=a1_OxbhRV89PrOZ-T0dMEfiGJAzEWSYosGDrTkStUO0,28731
21
- karrio/server/graph/schemas/base/types.py,sha256=KPN154yeFiIyFU0pfkhPjcP-h5Nr2hrzdQkDX--ufeU,46371
18
+ karrio/server/graph/schemas/base/__init__.py,sha256=J6pf4SzfAtQ4yaNapEuHuoyOA0-UGhueqDD3n0JzTb0,15118
19
+ karrio/server/graph/schemas/base/inputs.py,sha256=T1T7cNVGudgVGOkPnus6TfK_i-lT_cjOBlpaPTHummk,21363
20
+ karrio/server/graph/schemas/base/mutations.py,sha256=1S-aUpoKwwDslLtbReJb_5kx9tDr3-EfAhQQ9D7-6K0,33583
21
+ karrio/server/graph/schemas/base/types.py,sha256=lKC-mdLluLNaPn9DXQDxh7Vl8JT9b_s_CIqZkiO0NWo,46458
22
22
  karrio/server/graph/templates/graphql/graphiql.html,sha256=MQjQbBqoRE0QLsOUck8SaXo6B2oJO8dT6YZzUqbDan0,3786
23
23
  karrio/server/graph/templates/karrio/email_change_email.html,sha256=YHqTy9VGV_s7kio57Tg3v7TCIN3QlnPHi2ioTOcHJLE,467
24
24
  karrio/server/graph/templates/karrio/email_change_email.txt,sha256=NXXuzLR63hn2F1fVAjzmuguptuuTvujwqI7YLSrQoio,431
@@ -27,11 +27,12 @@ karrio/server/graph/tests/__init__.py,sha256=dPzsYY5hoO5gmY6fhL8tiz7Bfst8RB3JzsB
27
27
  karrio/server/graph/tests/base.py,sha256=m3k1CE-d9JHDsqfyXX0Fwksmkel33S7sh9-zkypp8wc,4004
28
28
  karrio/server/graph/tests/test_carrier_connections.py,sha256=PoXxJB53jBIz8j1GYCzTiaTW14Q0D1McdOkDYgcOqNM,6140
29
29
  karrio/server/graph/tests/test_metafield.py,sha256=K7Oon0CLEm_MUMbmcu0t2iAZvFN8Wl7Kp4QAWeUXo_Y,12783
30
- karrio/server/graph/tests/test_rate_sheets.py,sha256=iNdN0GR26wv8lZd7slUbHTs1HbasKlMk8gk-A-XRZ5c,10079
30
+ karrio/server/graph/tests/test_partial_shipments.py,sha256=dPIdIq4wiyovOaIIzbIX69eZnBqCA4ZvBSiGKYADM2g,19031
31
+ karrio/server/graph/tests/test_rate_sheets.py,sha256=cUzPV8dXQFPFh1r7W8bY6Lou3fjh8f9VGpyZrfbMXec,10300
31
32
  karrio/server/graph/tests/test_templates.py,sha256=WVU6vcfr6tEk917uSn1dECU8bkQtgD7FNuE-GJuFido,21626
32
33
  karrio/server/graph/tests/test_user_info.py,sha256=K91BL7SgxLWflCZriSVI8Vt5M5RIqmSCHKrgN2w8GmM,1928
33
34
  karrio/server/settings/graph.py,sha256=cz2yQHbp3xCfyFKuUkPEFfkI2fFVggExIY49wGz7mt0,106
34
- karrio_server_graph-2025.5rc11.dist-info/METADATA,sha256=9PSly89Cr9aY3qz-tZZFodyUPg_tvtF54Sc-yDcYfY4,744
35
- karrio_server_graph-2025.5rc11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
36
- karrio_server_graph-2025.5rc11.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
37
- karrio_server_graph-2025.5rc11.dist-info/RECORD,,
35
+ karrio_server_graph-2025.5rc13.dist-info/METADATA,sha256=qUDbtpb-Ih-hHRixZXLIiLxsPIcnsCJaqIf14lzGcJk,744
36
+ karrio_server_graph-2025.5rc13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ karrio_server_graph-2025.5rc13.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
38
+ karrio_server_graph-2025.5rc13.dist-info/RECORD,,