karrio-server-manager 2025.5rc1__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.
Files changed (102) hide show
  1. karrio/server/manager/__init__.py +1 -0
  2. karrio/server/manager/admin.py +1 -0
  3. karrio/server/manager/apps.py +13 -0
  4. karrio/server/manager/migrations/0001_initial.py +1358 -0
  5. karrio/server/manager/migrations/0002_auto_20201127_0721.py +61 -0
  6. karrio/server/manager/migrations/0003_auto_20201230_0820.py +34 -0
  7. karrio/server/manager/migrations/0004_auto_20210125_2125.py +18 -0
  8. karrio/server/manager/migrations/0005_auto_20210216_0758.py +27 -0
  9. karrio/server/manager/migrations/0006_auto_20210307_0438.py +24 -0
  10. karrio/server/manager/migrations/0006_auto_20210308_0302.py +53 -0
  11. karrio/server/manager/migrations/0007_merge_20210311_1428.py +14 -0
  12. karrio/server/manager/migrations/0008_remove_shipment_doc_images.py +17 -0
  13. karrio/server/manager/migrations/0009_auto_20210326_1425.py +28 -0
  14. karrio/server/manager/migrations/0010_auto_20210403_1404.py +28 -0
  15. karrio/server/manager/migrations/0011_auto_20210426_1924.py +48 -0
  16. karrio/server/manager/migrations/0012_auto_20210427_1319.py +24 -0
  17. karrio/server/manager/migrations/0013_customs_invoice_date.py +18 -0
  18. karrio/server/manager/migrations/0014_auto_20210515_0928.py +24 -0
  19. karrio/server/manager/migrations/0015_auto_20210601_0340.py +182 -0
  20. karrio/server/manager/migrations/0016_shipment_archived.py +18 -0
  21. karrio/server/manager/migrations/0017_auto_20210629_1650.py +22 -0
  22. karrio/server/manager/migrations/0018_auto_20210705_1049.py +23 -0
  23. karrio/server/manager/migrations/0019_auto_20210722_1131.py +43 -0
  24. karrio/server/manager/migrations/0020_tracking_messages.py +20 -0
  25. karrio/server/manager/migrations/0021_tracking_estimated_delivery.py +18 -0
  26. karrio/server/manager/migrations/0022_auto_20211122_2100.py +53 -0
  27. karrio/server/manager/migrations/0023_auto_20211227_2141.py +118 -0
  28. karrio/server/manager/migrations/0024_alter_parcel_items.py +18 -0
  29. karrio/server/manager/migrations/0025_auto_20220113_1158.py +25 -0
  30. karrio/server/manager/migrations/0026_parcel_reference_number.py +18 -0
  31. karrio/server/manager/migrations/0027_custom_migration_2021_1.py +47 -0
  32. karrio/server/manager/migrations/0028_auto_20220303_1153.py +39 -0
  33. karrio/server/manager/migrations/0029_auto_20220303_1249.py +55 -0
  34. karrio/server/manager/migrations/0030_alter_shipment_status.py +44 -0
  35. karrio/server/manager/migrations/0031_shipment_invoice.py +34 -0
  36. karrio/server/manager/migrations/0032_custom_migration_2022_3.py +26 -0
  37. karrio/server/manager/migrations/0033_auto_20220504_1335.py +57 -0
  38. karrio/server/manager/migrations/0034_commodity_hs_code.py +18 -0
  39. karrio/server/manager/migrations/0035_parcel_options.py +26 -0
  40. karrio/server/manager/migrations/0036_alter_tracking_shipment.py +24 -0
  41. karrio/server/manager/migrations/0037_auto_20220710_1350.py +28 -0
  42. karrio/server/manager/migrations/0038_alter_tracking_status.py +18 -0
  43. karrio/server/manager/migrations/0039_documentuploadrecord.py +43 -0
  44. karrio/server/manager/migrations/0040_parcel_freight_class.py +18 -0
  45. karrio/server/manager/migrations/0041_alter_commodity_options_alter_parcel_options.py +29 -0
  46. karrio/server/manager/migrations/0042_remove_shipment_shipment_tracking_number_idx_and_more.py +658 -0
  47. karrio/server/manager/migrations/0043_customs_duty_billing_address_and_more.py +62 -0
  48. karrio/server/manager/migrations/0044_address_address_line1_temp_and_more.py +326 -0
  49. karrio/server/manager/migrations/0045_alter_customs_duty_billing_address_and_more.py +45 -0
  50. karrio/server/manager/migrations/0046_auto_20230114_0930.py +78 -0
  51. karrio/server/manager/migrations/0047_remove_shipment_shipment_tracking_number_idx_and_more.py +595 -0
  52. karrio/server/manager/migrations/0048_commodity_title_alter_commodity_description_and_more.py +53 -0
  53. karrio/server/manager/migrations/0049_auto_20230318_0708.py +39 -0
  54. karrio/server/manager/migrations/0050_address_street_number_tracking_account_number_and_more.py +60 -0
  55. karrio/server/manager/migrations/0051_auto_20230330_0556.py +56 -0
  56. karrio/server/manager/migrations/0052_auto_20230520_0811.py +35 -0
  57. karrio/server/manager/migrations/0053_alter_commodity_weight_unit_alter_parcel_weight_unit.py +32 -0
  58. karrio/server/manager/migrations/0054_alter_address_company_name_alter_address_person_name.py +22 -0
  59. karrio/server/manager/migrations/0055_alter_tracking_status.py +32 -0
  60. karrio/server/manager/migrations/0056_tracking_delivery_image_tracking_signature_image.py +22 -0
  61. karrio/server/manager/migrations/0057_alter_customs_invoice_date.py +18 -0
  62. karrio/server/manager/migrations/0058_manifest_shipment_manifest.py +124 -0
  63. karrio/server/manager/migrations/0059_shipment_return_address.py +24 -0
  64. karrio/server/manager/migrations/0060_pickup_meta_alter_address_country_code_and_more.py +527 -0
  65. karrio/server/manager/migrations/0061_alter_customs_incoterm.py +37 -0
  66. karrio/server/manager/migrations/0062_alter_tracking_status.py +35 -0
  67. karrio/server/manager/migrations/__init__.py +0 -0
  68. karrio/server/manager/models.py +984 -0
  69. karrio/server/manager/router.py +3 -0
  70. karrio/server/manager/serializers/__init__.py +50 -0
  71. karrio/server/manager/serializers/address.py +82 -0
  72. karrio/server/manager/serializers/commodity.py +51 -0
  73. karrio/server/manager/serializers/customs.py +84 -0
  74. karrio/server/manager/serializers/document.py +113 -0
  75. karrio/server/manager/serializers/manifest.py +85 -0
  76. karrio/server/manager/serializers/parcel.py +84 -0
  77. karrio/server/manager/serializers/pickup.py +285 -0
  78. karrio/server/manager/serializers/rate.py +19 -0
  79. karrio/server/manager/serializers/shipment.py +869 -0
  80. karrio/server/manager/serializers/tracking.py +250 -0
  81. karrio/server/manager/signals.py +70 -0
  82. karrio/server/manager/tests/__init__.py +10 -0
  83. karrio/server/manager/tests/test_addresses.py +110 -0
  84. karrio/server/manager/tests/test_custom_infos.py +97 -0
  85. karrio/server/manager/tests/test_parcels.py +104 -0
  86. karrio/server/manager/tests/test_pickups.py +345 -0
  87. karrio/server/manager/tests/test_shipments.py +833 -0
  88. karrio/server/manager/tests/test_trackers.py +215 -0
  89. karrio/server/manager/urls.py +10 -0
  90. karrio/server/manager/views/__init__.py +9 -0
  91. karrio/server/manager/views/addresses.py +154 -0
  92. karrio/server/manager/views/customs.py +159 -0
  93. karrio/server/manager/views/documents.py +131 -0
  94. karrio/server/manager/views/manifests.py +160 -0
  95. karrio/server/manager/views/parcels.py +155 -0
  96. karrio/server/manager/views/pickups.py +182 -0
  97. karrio/server/manager/views/shipments.py +335 -0
  98. karrio/server/manager/views/trackers.py +364 -0
  99. karrio_server_manager-2025.5rc1.dist-info/METADATA +28 -0
  100. karrio_server_manager-2025.5rc1.dist-info/RECORD +102 -0
  101. karrio_server_manager-2025.5rc1.dist-info/WHEEL +5 -0
  102. karrio_server_manager-2025.5rc1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,250 @@
1
+ import typing
2
+ import logging
3
+ from django.utils import timezone
4
+
5
+ import karrio.lib as lib
6
+ import karrio.server.serializers as serializers
7
+ import karrio.server.core.utils as utils
8
+ from karrio.server.core.gateway import Shipments, Carriers
9
+ from karrio.server.core.serializers import (
10
+ TrackingDetails,
11
+ TrackingRequest,
12
+ ShipmentStatus,
13
+ TrackerStatus,
14
+ TrackingInfo,
15
+ )
16
+
17
+ import karrio.server.manager.models as models
18
+
19
+ logger = logging.getLogger(__name__)
20
+ DEFAULT_CARRIER_FILTER: typing.Any = dict(active=True, capability="tracking")
21
+
22
+
23
+ @serializers.owned_model_serializer
24
+ class TrackingSerializer(TrackingDetails):
25
+ carrier_id = serializers.CharField(required=False)
26
+ carrier_name = serializers.CharField(required=False)
27
+ test_mode = serializers.BooleanField(required=False)
28
+ options = serializers.PlainDictField(
29
+ required=False,
30
+ default={},
31
+ help_text="additional tracking options",
32
+ )
33
+ info = TrackingInfo(
34
+ required=False,
35
+ default={},
36
+ help_text="The package and shipment tracking details",
37
+ )
38
+ metadata = serializers.PlainDictField(
39
+ required=False,
40
+ default={},
41
+ help_text="The carrier user metadata.",
42
+ )
43
+
44
+ def create(self, validated_data: dict, context, **kwargs) -> models.Tracking:
45
+ options = validated_data["options"]
46
+ metadata = validated_data["metadata"]
47
+ carrier_filter = validated_data["carrier_filter"]
48
+ tracking_number = validated_data["tracking_number"]
49
+ account_number = validated_data.get("account_number")
50
+ info = validated_data.get("info")
51
+ reference = validated_data.get("reference")
52
+ pending_pickup = validated_data.get("pending_pickup")
53
+ carrier = Carriers.first(
54
+ context=context,
55
+ **{"raise_not_found": True, **DEFAULT_CARRIER_FILTER, **carrier_filter}
56
+ )
57
+
58
+ response = Shipments.track(
59
+ TrackingRequest(
60
+ dict(
61
+ tracking_numbers=[tracking_number],
62
+ account_number=account_number,
63
+ reference=reference,
64
+ options=options,
65
+ info=info,
66
+ )
67
+ ).data,
68
+ carrier=carrier,
69
+ raise_on_error=(pending_pickup is not True),
70
+ )
71
+
72
+ return models.Tracking.objects.create(
73
+ created_by=context.user,
74
+ tracking_number=tracking_number,
75
+ account_number=account_number,
76
+ events=lib.to_dict(response.tracking.events),
77
+ test_mode=response.tracking.test_mode,
78
+ delivered=response.tracking.delivered,
79
+ status=response.tracking.status,
80
+ tracking_carrier=carrier,
81
+ estimated_delivery=response.tracking.estimated_delivery,
82
+ messages=lib.to_dict(response.messages),
83
+ info=lib.to_dict(response.tracking.info),
84
+ meta=response.tracking.meta,
85
+ options=response.tracking.options,
86
+ reference=reference,
87
+ metadata=metadata,
88
+ delivery_image=getattr(response.tracking.images, "delivery_image", None),
89
+ signature_image=getattr(response.tracking.images, "signature_image", None),
90
+ )
91
+
92
+ def update(
93
+ self, instance: models.Tracking, validated_data: dict, context, **kwargs
94
+ ) -> models.Tracking:
95
+ last_fetch = (
96
+ timezone.now() - instance.updated_at
97
+ ).seconds / 60 # minutes since last fetch
98
+
99
+ if last_fetch >= 1 and instance.delivered is not True:
100
+ carrier_filter = validated_data["carrier_filter"]
101
+ options = {
102
+ instance.tracking_number: {
103
+ **(instance.options.get(instance.tracking_number) or {}),
104
+ **(
105
+ (validated_data.get("options") or {}).get(
106
+ instance.tracking_number
107
+ )
108
+ or {}
109
+ ),
110
+ }
111
+ }
112
+ carrier = (
113
+ Carriers.first(
114
+ context=context, **{**DEFAULT_CARRIER_FILTER, **carrier_filter}
115
+ )
116
+ or instance.tracking_carrier
117
+ )
118
+
119
+ response = Shipments.track(
120
+ payload=TrackingRequest(
121
+ dict(tracking_numbers=[instance.tracking_number], options=options)
122
+ ).data,
123
+ carrier=carrier,
124
+ )
125
+ # update values only if changed; This is important for webhooks notification
126
+ changes = []
127
+ details = response.tracking
128
+ info = lib.to_dict(details.info or {})
129
+ events = utils.process_events(
130
+ response_events=details.events, current_events=instance.events
131
+ )
132
+
133
+ if events != instance.events:
134
+ instance.events = events
135
+ changes.append("events")
136
+
137
+ if response.messages != instance.messages:
138
+ instance.messages = lib.to_dict(response.messages)
139
+ changes.append("messages")
140
+
141
+ if details.delivered != instance.delivered:
142
+ instance.delivered = details.delivered
143
+ changes.append("delivered")
144
+
145
+ if details.status != instance.status:
146
+ instance.status = details.status
147
+ changes.append("status")
148
+
149
+ if details.estimated_delivery != instance.estimated_delivery:
150
+ instance.estimated_delivery = details.estimated_delivery
151
+ changes.append("estimated_delivery")
152
+
153
+ if details.options != instance.options:
154
+ instance.options = details.options
155
+ changes.append("options")
156
+
157
+ if any(info.keys()) and info != instance.info:
158
+ instance.info = serializers.process_dictionaries_mutations(
159
+ ["info"], dict(info=info), instance
160
+ )["info"]
161
+ changes.append("info")
162
+
163
+ if carrier.id != instance.tracking_carrier.id:
164
+ instance.carrier = carrier
165
+ changes.append("tracking_carrier")
166
+
167
+ if details.images is not None and (
168
+ details.images.delivery_image != instance.delivery_image
169
+ or details.images.signature_image != instance.signature_image
170
+ ):
171
+ changes.append("delivery_image")
172
+ changes.append("signature_image")
173
+ instance.delivery_image = (
174
+ details.images.delivery_image or instance.delivery_image
175
+ )
176
+ instance.signature_image = (
177
+ details.images.signature_image or instance.signature_image
178
+ )
179
+
180
+ if any(changes):
181
+ instance.save(update_fields=changes)
182
+ update_shipment_tracker(instance)
183
+
184
+ return instance
185
+
186
+
187
+ @serializers.owned_model_serializer
188
+ class TrackerUpdateData(serializers.Serializer):
189
+ info = TrackingInfo(
190
+ required=False,
191
+ allow_null=True,
192
+ help_text="The package and shipment tracking details",
193
+ )
194
+ metadata = serializers.PlainDictField(
195
+ required=False, help_text="User metadata for the tracker"
196
+ )
197
+
198
+ def update(
199
+ self, instance: models.Tracking, validated_data: dict, **kwargs
200
+ ) -> models.Tracking:
201
+ changes = []
202
+ data = validated_data.copy()
203
+
204
+ for key, val in data.items():
205
+ if key in models.Tracking.DIRECT_PROPS and getattr(instance, key) != val:
206
+ setattr(instance, key, val)
207
+ changes.append(key)
208
+ validated_data.pop(key)
209
+
210
+ if any(changes):
211
+ instance.save(update_fields=changes)
212
+
213
+ return instance
214
+
215
+
216
+ def can_mutate_tracker(
217
+ tracker: models.Tracking,
218
+ update: bool = False,
219
+ payload: dict = None,
220
+ ):
221
+ if update and tracker.delivered and [*(payload or {}).keys()] == ["metadata"]:
222
+ return
223
+
224
+ if update and all([key in ["metadata", "info"] for key in (payload or {}).keys()]):
225
+ return
226
+
227
+
228
+ def update_shipment_tracker(tracker: models.Tracking):
229
+ try:
230
+ if tracker.status == TrackerStatus.delivered.value:
231
+ status = ShipmentStatus.delivered.value
232
+ elif tracker.status == TrackerStatus.pending.value:
233
+ status = tracker.shipment.status
234
+ elif tracker.status == TrackerStatus.out_for_delivery.value:
235
+ status = ShipmentStatus.out_for_delivery.value
236
+ elif tracker.status == TrackerStatus.delivery_failed.value:
237
+ status = ShipmentStatus.delivery_failed.value
238
+ elif tracker.status in [
239
+ TrackerStatus.on_hold.value,
240
+ TrackerStatus.delivery_delayed.value,
241
+ ]:
242
+ status = ShipmentStatus.needs_attention.value
243
+ else:
244
+ status = ShipmentStatus.in_transit.value
245
+
246
+ if tracker.shipment is not None and tracker.shipment.status != status:
247
+ tracker.shipment.status = status
248
+ tracker.shipment.save(update_fields=["status"])
249
+ except Exception as e:
250
+ logger.exception("Failed to update the tracked shipment", e)
@@ -0,0 +1,70 @@
1
+ import logging
2
+ from django.db.models import signals
3
+
4
+ from karrio.server.core import utils
5
+ import karrio.server.manager.models as models
6
+ import karrio.server.manager.serializers as serializers
7
+
8
+ logger = logging.getLogger(__name__)
9
+ RATE_RELATED_CHANGES = [
10
+ # address related changes
11
+ "address_line1",
12
+ "address_line2",
13
+ "postal_code",
14
+ "city",
15
+ "state_province",
16
+ "country_code",
17
+ "residential",
18
+ # parcel related changes
19
+ "length",
20
+ "width",
21
+ "height",
22
+ "weight",
23
+ "weight_unit",
24
+ "dimension_unit",
25
+ "is_document",
26
+ ]
27
+
28
+
29
+ def register_signals():
30
+ signals.post_save.connect(address_updated, sender=models.Address)
31
+ signals.post_save.connect(parcel_updated, sender=models.Parcel)
32
+ signals.post_delete.connect(parcel_deleted, sender=models.Parcel)
33
+
34
+ logger.info("karrio.manager signals registered...")
35
+
36
+
37
+ @utils.disable_for_loaddata
38
+ def address_updated(
39
+ sender, instance, created, raw, using, update_fields, *args, **kwargs
40
+ ):
41
+ """ """
42
+ changes = update_fields or []
43
+
44
+ if any([change in RATE_RELATED_CHANGES for change in changes]):
45
+ serializers.reset_related_shipment_rates(instance.shipment)
46
+
47
+
48
+ @utils.disable_for_loaddata
49
+ def parcel_updated(
50
+ sender, instance, created, raw, using, update_fields, *args, **kwargs
51
+ ):
52
+ """ """
53
+ changes = update_fields or []
54
+
55
+ if instance.reference_number is None:
56
+ count = models.Parcel.objects.filter(
57
+ **({"org__id": instance.link.org.id} if hasattr(instance, "link") else {})
58
+ ).count()
59
+
60
+ instance.reference_number = str(count + 1).zfill(10)
61
+ instance.save()
62
+
63
+ if any([change in RATE_RELATED_CHANGES for change in changes]):
64
+ serializers.reset_related_shipment_rates(instance.shipment)
65
+
66
+
67
+ @utils.disable_for_loaddata
68
+ def parcel_deleted(sender, instance, *args, **kwargs):
69
+ """ """
70
+ serializers.reset_related_shipment_rates(instance.shipment)
@@ -0,0 +1,10 @@
1
+ import logging
2
+
3
+ logging.disable(logging.CRITICAL)
4
+
5
+ from karrio.server.manager.tests.test_addresses import *
6
+ from karrio.server.manager.tests.test_parcels import *
7
+ from karrio.server.manager.tests.test_shipments import *
8
+ from karrio.server.manager.tests.test_trackers import *
9
+ from karrio.server.manager.tests.test_custom_infos import *
10
+ from karrio.server.manager.tests.test_pickups import *
@@ -0,0 +1,110 @@
1
+ import json
2
+ from unittest.mock import ANY
3
+ from django.urls import reverse
4
+ from rest_framework import status
5
+ from karrio.server.core.tests import APITestCase
6
+ from karrio.server.manager.models import Address
7
+
8
+
9
+ class TestAddresses(APITestCase):
10
+ def test_create_address(self):
11
+ url = reverse("karrio.server.manager:address-list")
12
+ data = ADDRESS_DATA
13
+
14
+ response = self.client.post(url, data)
15
+ response_data = json.loads(response.content)
16
+
17
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
18
+ self.assertDictEqual(response_data, ADDRESS_RESPONSE)
19
+
20
+
21
+ class TestAddressDetails(APITestCase):
22
+ def setUp(self) -> None:
23
+ super().setUp()
24
+ self.address: Address = Address.objects.create(
25
+ **{
26
+ "address_line1": "5205 rue riviera",
27
+ "person_name": "Old town Daniel",
28
+ "phone_number": "438 222 2222",
29
+ "city": "Montreal",
30
+ "country_code": "CA",
31
+ "postal_code": "H8Z2Z3",
32
+ "residential": True,
33
+ "state_code": "QC",
34
+ "validate_location": False,
35
+ "validation": None,
36
+ "created_by": self.user,
37
+ }
38
+ )
39
+
40
+ def test_update_address(self):
41
+ url = reverse(
42
+ "karrio.server.manager:address-details", kwargs=dict(pk=self.address.pk)
43
+ )
44
+ data = ADDRESS_UPDATE_DATA
45
+
46
+ response = self.client.patch(url, data)
47
+ response_data = json.loads(response.content)
48
+
49
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
50
+ self.assertDictEqual(response_data, ADDRESS_UPDATE_RESPONSE)
51
+
52
+
53
+ ADDRESS_DATA = {
54
+ "address_line1": "5205 rue riviera",
55
+ "person_name": "Old town Daniel",
56
+ "phone_number": "438 222 2222",
57
+ "city": "Montreal",
58
+ "country_code": "CA",
59
+ "postal_code": "H8Z2Z3",
60
+ "residential": True,
61
+ "state_code": "QC",
62
+ }
63
+
64
+ ADDRESS_RESPONSE = {
65
+ "id": ANY,
66
+ "object_type": "address",
67
+ "postal_code": "H8Z2Z3",
68
+ "city": "Montreal",
69
+ "federal_tax_id": None,
70
+ "state_tax_id": None,
71
+ "person_name": "Old town Daniel",
72
+ "company_name": None,
73
+ "country_code": "CA",
74
+ "email": None,
75
+ "phone_number": "+1 438-222-2222",
76
+ "state_code": "QC",
77
+ "street_number": None,
78
+ "residential": True,
79
+ "address_line1": "5205 rue riviera",
80
+ "address_line2": None,
81
+ "validate_location": False,
82
+ "validation": None,
83
+ }
84
+
85
+ ADDRESS_UPDATE_DATA = {
86
+ "person_name": "John Doe",
87
+ "company_name": "Doe corp",
88
+ "residential": False,
89
+ }
90
+
91
+ ADDRESS_UPDATE_RESPONSE = {
92
+ "id": ANY,
93
+ "object_type": "address",
94
+ "postal_code": "H8Z2Z3",
95
+ "city": "Montreal",
96
+ "federal_tax_id": None,
97
+ "state_tax_id": None,
98
+ "person_name": "John Doe",
99
+ "company_name": "Doe corp",
100
+ "country_code": "CA",
101
+ "email": None,
102
+ "phone_number": "438 222 2222",
103
+ "state_code": "QC",
104
+ "street_number": None,
105
+ "residential": False,
106
+ "address_line1": "5205 rue riviera",
107
+ "address_line2": None,
108
+ "validate_location": False,
109
+ "validation": None,
110
+ }
@@ -0,0 +1,97 @@
1
+ import json
2
+ from unittest.mock import ANY
3
+ from django.urls import reverse
4
+ from rest_framework import status
5
+ from karrio.server.core.tests import APITestCase
6
+ from karrio.server.manager.models import Customs
7
+
8
+
9
+ class TestCustomsInfo(APITestCase):
10
+ def test_create_customs(self):
11
+ url = reverse("karrio.server.manager:customs-list")
12
+ data = CUSTOMS_DATA
13
+
14
+ response = self.client.post(url, data)
15
+ response_data = json.loads(response.content)
16
+
17
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
18
+ self.assertDictEqual(response_data, CUSTOMS_RESPONSE)
19
+
20
+
21
+ class TestCustomsInfoDetails(APITestCase):
22
+ def setUp(self) -> None:
23
+ super().setUp()
24
+ self.customs: Customs = Customs.objects.create(**{"created_by": self.user})
25
+
26
+ def test_update_customs(self):
27
+ url = reverse(
28
+ "karrio.server.manager:customs-details", kwargs=dict(pk=self.customs.pk)
29
+ )
30
+ data = CUSTOMS_UPDATE_DATA
31
+
32
+ response = self.client.patch(url, data)
33
+ response_data = json.loads(response.content)
34
+
35
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
36
+ self.assertDictEqual(response_data, CUSTOMS_UPDATE_RESPONSE)
37
+
38
+
39
+ CUSTOMS_DATA = {
40
+ "incoterm": "DDU",
41
+ "commodities": [
42
+ {"title": "cn", "weight": 4.0, "weight_unit": "KG", "sku": "cc"},
43
+ ],
44
+ }
45
+
46
+ CUSTOMS_RESPONSE = {
47
+ "certify": None,
48
+ "commercial_invoice": None,
49
+ "commodities": [
50
+ {
51
+ "description": None,
52
+ "title": "cn",
53
+ "id": ANY,
54
+ "object_type": "commodity",
55
+ "origin_country": None,
56
+ "parent_id": None,
57
+ "quantity": 1,
58
+ "metadata": {},
59
+ "sku": "cc",
60
+ "hs_code": None,
61
+ "value_amount": None,
62
+ "value_currency": None,
63
+ "weight": 4.0,
64
+ "weight_unit": "KG",
65
+ }
66
+ ],
67
+ "content_description": None,
68
+ "content_type": None,
69
+ "duty": None,
70
+ "duty_billing_address": None,
71
+ "id": ANY,
72
+ "object_type": "customs_info",
73
+ "incoterm": "DDU",
74
+ "invoice": None,
75
+ "invoice_date": None,
76
+ "options": {},
77
+ "signer": None,
78
+ }
79
+
80
+ CUSTOMS_UPDATE_DATA = {"incoterm": "DDP"}
81
+
82
+ CUSTOMS_UPDATE_RESPONSE = {
83
+ "certify": None,
84
+ "commercial_invoice": None,
85
+ "commodities": [],
86
+ "content_description": None,
87
+ "content_type": None,
88
+ "duty": None,
89
+ "duty_billing_address": None,
90
+ "id": ANY,
91
+ "object_type": "customs_info",
92
+ "incoterm": "DDP",
93
+ "invoice": None,
94
+ "invoice_date": None,
95
+ "options": {},
96
+ "signer": None,
97
+ }
@@ -0,0 +1,104 @@
1
+ import json
2
+ from unittest.mock import ANY
3
+ from django.urls import reverse
4
+ from rest_framework import status
5
+ from karrio.server.core.tests import APITestCase
6
+ from karrio.server.manager.models import Parcel
7
+
8
+
9
+ class TestParcels(APITestCase):
10
+ def test_create_parcel(self):
11
+ url = reverse("karrio.server.manager:parcel-list")
12
+ data = PARCEL_DATA
13
+
14
+ response = self.client.post(url, data)
15
+ response_data = json.loads(response.content)
16
+
17
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
18
+ self.assertDictEqual(response_data, PARCEL_RESPONSE)
19
+
20
+
21
+ class TestParcelDetails(APITestCase):
22
+ def setUp(self) -> None:
23
+ super().setUp()
24
+ self.parcel: Parcel = Parcel.objects.create(
25
+ **{
26
+ "weight": 1,
27
+ "width": 20,
28
+ "height": 10,
29
+ "length": 29,
30
+ "weight_unit": "KG",
31
+ "dimension_unit": "CM",
32
+ "created_by": self.user,
33
+ }
34
+ )
35
+
36
+ def test_update_parcel(self):
37
+ url = reverse(
38
+ "karrio.server.manager:parcel-details", kwargs=dict(pk=self.parcel.pk)
39
+ )
40
+ data = PARCEL_UPDATE_DATA
41
+
42
+ response = self.client.patch(url, data)
43
+ response_data = json.loads(response.content)
44
+
45
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
46
+ self.assertDictEqual(response_data, PARCEL_UPDATE_RESPONSE)
47
+
48
+
49
+ PARCEL_DATA = {
50
+ "weight": 1,
51
+ "width": 20,
52
+ "height": 10,
53
+ "length": 29,
54
+ "weight_unit": "KG",
55
+ "dimension_unit": "CM",
56
+ }
57
+
58
+ PARCEL_RESPONSE = {
59
+ "id": ANY,
60
+ "object_type": "parcel",
61
+ "weight": 1.0,
62
+ "width": 20.0,
63
+ "height": 10.0,
64
+ "length": 29.0,
65
+ "packaging_type": None,
66
+ "package_preset": None,
67
+ "description": None,
68
+ "content": None,
69
+ "is_document": False,
70
+ "items": [],
71
+ "weight_unit": "KG",
72
+ "dimension_unit": "CM",
73
+ "freight_class": None,
74
+ "reference_number": ANY,
75
+ "options": {},
76
+ }
77
+
78
+ PARCEL_UPDATE_DATA = {
79
+ "width": 5.0,
80
+ "height": 4.5,
81
+ "length": 10.0,
82
+ "weight_unit": "LB",
83
+ "dimension_unit": "IN",
84
+ }
85
+
86
+ PARCEL_UPDATE_RESPONSE = {
87
+ "id": ANY,
88
+ "object_type": "parcel",
89
+ "weight": 1.0,
90
+ "width": 5.0,
91
+ "height": 4.5,
92
+ "length": 10.0,
93
+ "packaging_type": None,
94
+ "package_preset": None,
95
+ "description": None,
96
+ "content": None,
97
+ "is_document": False,
98
+ "items": [],
99
+ "weight_unit": "LB",
100
+ "dimension_unit": "IN",
101
+ "freight_class": None,
102
+ "reference_number": ANY,
103
+ "options": {},
104
+ }