karrio-server-graph 2026.1__py3-none-any.whl → 2026.1.3__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.
@@ -68,10 +68,33 @@ class WorkspaceConfigModelSerializer(serializers.ModelSerializer):
68
68
 
69
69
  @serializers.owned_model_serializer
70
70
  class MetafieldModelSerializer(serializers.ModelSerializer):
71
+ object_type = serializers.CharField(required=False, allow_null=True)
72
+ object_id = serializers.CharField(required=False, allow_null=True)
73
+
71
74
  class Meta:
72
75
  model = core.Metafield
73
76
  extra_kwargs = {field: {"read_only": True} for field in ["id"]}
74
- exclude = ["created_at", "updated_at", "created_by"]
77
+ exclude = ["created_at", "updated_at", "created_by", "content_type"]
78
+
79
+ def validate(self, data):
80
+ from django.contrib.contenttypes.models import ContentType
81
+
82
+ object_type = data.pop("object_type", None)
83
+ object_id = data.get("object_id")
84
+
85
+ if object_type and object_id:
86
+ ct = ContentType.objects.filter(model=object_type).first()
87
+ if not ct:
88
+ raise serializers.ValidationError(
89
+ {"object_type": f"Invalid object type: {object_type}"}
90
+ )
91
+ data["content_type"] = ct
92
+ elif object_type or object_id:
93
+ raise serializers.ValidationError(
94
+ "Both object_type and object_id must be provided together"
95
+ )
96
+
97
+ return data
75
98
 
76
99
 
77
100
  @serializers.owned_model_serializer
@@ -98,53 +121,6 @@ class CommodityModelSerializer(serializers.ModelSerializer):
98
121
  extra_kwargs = {field: {"read_only": True} for field in ["id", "parent"]}
99
122
 
100
123
 
101
- @serializers.owned_model_serializer
102
- class CustomsModelSerializer(serializers.ModelSerializer):
103
- NESTED_FIELDS = ["commodities"]
104
-
105
- incoterm = serializers.CharField(required=False, allow_null=True, allow_blank=True)
106
- commodities = serializers.make_fields_optional(CommodityModelSerializer)(
107
- many=True, allow_null=True, required=False
108
- )
109
-
110
- class Meta:
111
- model = manager.Customs
112
- exclude = ["created_at", "updated_at", "created_by"]
113
- extra_kwargs = {field: {"read_only": True} for field in ["id"]}
114
-
115
- @transaction.atomic
116
- def create(self, validated_data: dict, context: dict):
117
- data = {
118
- name: value
119
- for name, value in validated_data.items()
120
- if name not in self.NESTED_FIELDS
121
- }
122
-
123
- instance = super().create(data)
124
-
125
- serializers.save_many_to_many_data(
126
- "commodities",
127
- CommodityModelSerializer,
128
- instance,
129
- payload=validated_data,
130
- context=context,
131
- )
132
-
133
- return instance
134
-
135
- @transaction.atomic
136
- def update(
137
- self, instance: manager.Customs, validated_data: dict, **kwargs
138
- ) -> manager.Customs:
139
- data = {
140
- name: value
141
- for name, value in validated_data.items()
142
- if name not in self.NESTED_FIELDS
143
- }
144
-
145
- return super().update(instance, data)
146
-
147
-
148
124
  @serializers.owned_model_serializer
149
125
  class ParcelModelSerializer(validators.PresetSerializer, serializers.ModelSerializer):
150
126
  weight_unit = serializers.CharField(
@@ -163,7 +139,6 @@ class ParcelModelSerializer(validators.PresetSerializer, serializers.ModelSerial
163
139
  @serializers.owned_model_serializer
164
140
  class TemplateModelSerializer(serializers.ModelSerializer):
165
141
  address = serializers.make_fields_optional(AddressModelSerializer)(required=False)
166
- customs = serializers.make_fields_optional(CustomsModelSerializer)(required=False)
167
142
  parcel = serializers.make_fields_optional(ParcelModelSerializer)(required=False)
168
143
 
169
144
  class Meta:
@@ -181,12 +156,6 @@ class TemplateModelSerializer(serializers.ModelSerializer):
181
156
  payload=validated_data,
182
157
  context=context,
183
158
  ),
184
- "customs": serializers.save_one_to_one_data(
185
- "customs",
186
- CustomsModelSerializer,
187
- payload=validated_data,
188
- context=context,
189
- ),
190
159
  "parcel": serializers.save_one_to_one_data(
191
160
  "parcel", ParcelModelSerializer, payload=validated_data, context=context
192
161
  ),
@@ -203,15 +172,12 @@ class TemplateModelSerializer(serializers.ModelSerializer):
203
172
  data = {
204
173
  key: value
205
174
  for key, value in validated_data.items()
206
- if key not in ["address", "customs", "parcel"]
175
+ if key not in ["address", "parcel"]
207
176
  }
208
177
 
209
178
  serializers.save_one_to_one_data(
210
179
  "address", AddressModelSerializer, instance, payload=validated_data
211
180
  )
212
- serializers.save_one_to_one_data(
213
- "customs", CustomsModelSerializer, instance, payload=validated_data
214
- )
215
181
  serializers.save_one_to_one_data(
216
182
  "parcel", ParcelModelSerializer, instance, payload=validated_data
217
183
  )
@@ -52,7 +52,7 @@ class GraphTestCase(BaseAPITestCase):
52
52
  self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token.key)
53
53
 
54
54
  # Setup test carrier connections.
55
- self.carrier = providers.Carrier.objects.create(
55
+ self.carrier = providers.CarrierConnection.objects.create(
56
56
  carrier_code="canadapost",
57
57
  carrier_id="canadapost",
58
58
  test_mode=False,
@@ -65,7 +65,7 @@ class GraphTestCase(BaseAPITestCase):
65
65
  ),
66
66
  capabilities=["pickup", "rating", "tracking", "shipping"],
67
67
  )
68
- self.ups_carrier = providers.Carrier.objects.create(
68
+ self.ups_carrier = providers.CarrierConnection.objects.create(
69
69
  carrier_code="ups",
70
70
  carrier_id="ups_package",
71
71
  test_mode=False,
@@ -77,10 +77,11 @@ class GraphTestCase(BaseAPITestCase):
77
77
  ),
78
78
  capabilities=["pickup", "rating", "tracking", "shipping"],
79
79
  )
80
- self.fedex_carrier = providers.Carrier.objects.create(
80
+ self.fedex_carrier = providers.CarrierConnection.objects.create(
81
81
  carrier_code="fedex",
82
82
  carrier_id="fedex_express",
83
83
  test_mode=False,
84
+ created_by=self.user,
84
85
  credentials=dict(
85
86
  api_key="test",
86
87
  secret_key="password",
@@ -89,13 +90,12 @@ class GraphTestCase(BaseAPITestCase):
89
90
  track_secret_key="password",
90
91
  ),
91
92
  capabilities=["pickup", "rating", "tracking", "shipping"],
92
- is_system=True,
93
93
  )
94
- self.dhl_carrier = providers.Carrier.objects.create(
94
+ self.dhl_carrier = providers.CarrierConnection.objects.create(
95
95
  carrier_code="dhl_universal",
96
96
  carrier_id="dhl_universal",
97
97
  test_mode=False,
98
- is_system=True,
98
+ created_by=self.user,
99
99
  credentials=dict(
100
100
  consumer_key="test",
101
101
  consumer_secret="password",
@@ -103,6 +103,33 @@ class GraphTestCase(BaseAPITestCase):
103
103
  capabilities=["tracking"],
104
104
  )
105
105
 
106
+ # Setup system connections for system_connections queries
107
+ self.dhl_system_connection = providers.SystemConnection.objects.create(
108
+ carrier_code="dhl_universal",
109
+ carrier_id="dhl_universal",
110
+ test_mode=False,
111
+ active=True,
112
+ credentials=dict(
113
+ consumer_key="system_test",
114
+ consumer_secret="system_password",
115
+ ),
116
+ capabilities=["tracking"],
117
+ )
118
+ self.fedex_system_connection = providers.SystemConnection.objects.create(
119
+ carrier_code="fedex",
120
+ carrier_id="fedex_express",
121
+ test_mode=False,
122
+ active=True,
123
+ credentials=dict(
124
+ api_key="system_test",
125
+ secret_key="system_password",
126
+ account_number="000000",
127
+ track_api_key="system_test",
128
+ track_secret_key="system_password",
129
+ ),
130
+ capabilities=["pickup", "rating", "tracking", "shipping"],
131
+ )
132
+
106
133
  def query(
107
134
  self,
108
135
  query: str,
@@ -121,8 +121,8 @@ SYSTEM_CONNECTIONS = {
121
121
  {
122
122
  "node": {
123
123
  "active": True,
124
- "carrier_id": "dhl_universal",
125
- "carrier_name": "dhl_universal",
124
+ "carrier_id": "fedex_express",
125
+ "carrier_name": "fedex",
126
126
  "id": ANY,
127
127
  "test_mode": False,
128
128
  }
@@ -130,8 +130,8 @@ SYSTEM_CONNECTIONS = {
130
130
  {
131
131
  "node": {
132
132
  "active": True,
133
- "carrier_id": "fedex_express",
134
- "carrier_name": "fedex",
133
+ "carrier_id": "dhl_universal",
134
+ "carrier_name": "dhl_universal",
135
135
  "id": ANY,
136
136
  "test_mode": False,
137
137
  }
@@ -145,6 +145,35 @@ USER_CONNECTIONS = {
145
145
  "data": {
146
146
  "user_connections": {
147
147
  "edges": [
148
+ {
149
+ "node": {
150
+ "active": True,
151
+ "carrier_id": "dhl_universal",
152
+ "carrier_name": "dhl_universal",
153
+ "credentials": {
154
+ "consumer_key": "test",
155
+ "consumer_secret": "password",
156
+ },
157
+ "id": ANY,
158
+ "test_mode": False,
159
+ }
160
+ },
161
+ {
162
+ "node": {
163
+ "active": True,
164
+ "carrier_id": "fedex_express",
165
+ "carrier_name": "fedex",
166
+ "credentials": {
167
+ "account_number": "000000",
168
+ "api_key": "test",
169
+ "secret_key": "password",
170
+ "track_api_key": "test",
171
+ "track_secret_key": "password",
172
+ },
173
+ "id": ANY,
174
+ "test_mode": False,
175
+ }
176
+ },
148
177
  {
149
178
  "node": {
150
179
  "active": True,
@@ -0,0 +1,333 @@
1
+ """GraphQL pickup query tests following AGENTS.md patterns."""
2
+
3
+ import karrio.lib as lib
4
+ from karrio.server.graph.tests.base import GraphTestCase
5
+ from karrio.server.core.utils import create_carrier_snapshot
6
+ import karrio.server.manager.models as models
7
+
8
+
9
+ class TestPickupQueries(GraphTestCase):
10
+ def setUp(self):
11
+ super().setUp()
12
+ self.maxDiff = None
13
+
14
+ # Create a shipment for the pickup
15
+ self.shipment = models.Shipment.objects.create(
16
+ shipper={
17
+ "postal_code": "E1C4Z8",
18
+ "city": "Moncton",
19
+ "person_name": "John Doe",
20
+ "company_name": "A corp.",
21
+ "country_code": "CA",
22
+ "phone_number": "514 000 0000",
23
+ "state_code": "NB",
24
+ "address_line1": "125 Church St",
25
+ },
26
+ recipient={
27
+ "postal_code": "V6M2V9",
28
+ "city": "Vancouver",
29
+ "person_name": "Jane Doe",
30
+ "company_name": "B corp.",
31
+ "country_code": "CA",
32
+ "phone_number": "604 000 0000",
33
+ "state_code": "BC",
34
+ "address_line1": "5840 Oak St",
35
+ },
36
+ parcels=[
37
+ {
38
+ "weight": 1.0,
39
+ "weight_unit": "KG",
40
+ "package_preset": "canadapost_corrugated_small_box",
41
+ }
42
+ ],
43
+ created_by=self.user,
44
+ test_mode=False,
45
+ tracking_number="123456789012",
46
+ carrier=create_carrier_snapshot(self.carrier),
47
+ )
48
+
49
+ # Create test pickup (test_mode must match token's test_mode)
50
+ self.pickup = models.Pickup.objects.create(
51
+ address={
52
+ "id": "adr_001122334455",
53
+ "postal_code": "E1C4Z8",
54
+ "city": "Moncton",
55
+ "person_name": "John Poop",
56
+ "company_name": "A corp.",
57
+ "country_code": "CA",
58
+ "phone_number": "514 000 0000",
59
+ "state_code": "NB",
60
+ "address_line1": "125 Church St",
61
+ },
62
+ carrier=create_carrier_snapshot(self.carrier),
63
+ created_by=self.user,
64
+ test_mode=False,
65
+ pickup_date="2020-10-25",
66
+ ready_time="13:00",
67
+ closing_time="17:00",
68
+ instruction="Should not be folded",
69
+ package_location="At the main entrance hall",
70
+ confirmation_number="00110215",
71
+ pickup_charge={"name": "Pickup fees", "amount": 0.0, "currency": "CAD"},
72
+ )
73
+ self.pickup.shipments.set([self.shipment])
74
+
75
+ def test_query_pickup(self):
76
+ """Verify single pickup query."""
77
+ response = self.query(
78
+ """
79
+ query get_pickup($id: String!) {
80
+ pickup(id: $id) {
81
+ id
82
+ object_type
83
+ confirmation_number
84
+ pickup_date
85
+ ready_time
86
+ closing_time
87
+ carrier_name
88
+ carrier_id
89
+ test_mode
90
+ }
91
+ }
92
+ """,
93
+ operation_name="get_pickup",
94
+ variables={"id": self.pickup.id},
95
+ )
96
+
97
+ self.assertResponseNoErrors(response)
98
+ self.assertDictEqual(
99
+ lib.to_dict(response.data),
100
+ {
101
+ "data": {
102
+ "pickup": {
103
+ "id": self.pickup.id,
104
+ "object_type": "pickup",
105
+ "confirmation_number": "00110215",
106
+ "pickup_date": "2020-10-25",
107
+ "ready_time": "13:00",
108
+ "closing_time": "17:00",
109
+ "carrier_name": "canadapost",
110
+ "carrier_id": "canadapost",
111
+ "test_mode": False,
112
+ }
113
+ }
114
+ },
115
+ )
116
+
117
+ def test_query_pickup_with_address(self):
118
+ """Verify pickup query includes address."""
119
+ response = self.query(
120
+ """
121
+ query get_pickup($id: String!) {
122
+ pickup(id: $id) {
123
+ id
124
+ address {
125
+ city
126
+ country_code
127
+ postal_code
128
+ }
129
+ }
130
+ }
131
+ """,
132
+ operation_name="get_pickup",
133
+ variables={"id": self.pickup.id},
134
+ )
135
+
136
+ self.assertResponseNoErrors(response)
137
+ self.assertEqual(
138
+ response.data["data"]["pickup"]["address"]["city"],
139
+ "Moncton",
140
+ )
141
+ self.assertEqual(
142
+ response.data["data"]["pickup"]["address"]["country_code"],
143
+ "CA",
144
+ )
145
+
146
+ def test_query_pickup_with_tracking_numbers(self):
147
+ """Verify pickup query includes tracking numbers from shipments."""
148
+ response = self.query(
149
+ """
150
+ query get_pickup($id: String!) {
151
+ pickup(id: $id) {
152
+ id
153
+ tracking_numbers
154
+ }
155
+ }
156
+ """,
157
+ operation_name="get_pickup",
158
+ variables={"id": self.pickup.id},
159
+ )
160
+
161
+ self.assertResponseNoErrors(response)
162
+ self.assertEqual(
163
+ response.data["data"]["pickup"]["tracking_numbers"],
164
+ ["123456789012"],
165
+ )
166
+
167
+ def test_query_pickups_list(self):
168
+ """Verify pickup list query with pagination."""
169
+ response = self.query(
170
+ """
171
+ query get_pickups($filter: PickupFilter) {
172
+ pickups(filter: $filter) {
173
+ page_info {
174
+ count
175
+ has_next_page
176
+ }
177
+ edges {
178
+ node {
179
+ id
180
+ confirmation_number
181
+ carrier_name
182
+ }
183
+ }
184
+ }
185
+ }
186
+ """,
187
+ operation_name="get_pickups",
188
+ variables={"filter": {"first": 20, "offset": 0}},
189
+ )
190
+
191
+ self.assertResponseNoErrors(response)
192
+ self.assertEqual(response.data["data"]["pickups"]["page_info"]["count"], 1)
193
+ self.assertEqual(
194
+ response.data["data"]["pickups"]["edges"][0]["node"]["confirmation_number"],
195
+ "00110215",
196
+ )
197
+
198
+ def test_query_pickups_filter_by_carrier(self):
199
+ """Verify pickup filtering by carrier_name."""
200
+ response = self.query(
201
+ """
202
+ query get_pickups($filter: PickupFilter) {
203
+ pickups(filter: $filter) {
204
+ edges {
205
+ node {
206
+ id
207
+ carrier_name
208
+ }
209
+ }
210
+ }
211
+ }
212
+ """,
213
+ operation_name="get_pickups",
214
+ variables={"filter": {"carrier_name": ["canadapost"]}},
215
+ )
216
+
217
+ self.assertResponseNoErrors(response)
218
+ self.assertEqual(len(response.data["data"]["pickups"]["edges"]), 1)
219
+
220
+ def test_query_pickups_filter_by_carrier_no_match(self):
221
+ """Verify pickup filtering with non-matching carrier returns empty."""
222
+ response = self.query(
223
+ """
224
+ query get_pickups($filter: PickupFilter) {
225
+ pickups(filter: $filter) {
226
+ edges {
227
+ node {
228
+ id
229
+ carrier_name
230
+ }
231
+ }
232
+ }
233
+ }
234
+ """,
235
+ operation_name="get_pickups",
236
+ variables={"filter": {"carrier_name": ["ups"]}},
237
+ )
238
+
239
+ self.assertResponseNoErrors(response)
240
+ self.assertEqual(len(response.data["data"]["pickups"]["edges"]), 0)
241
+
242
+ def test_query_pickups_filter_by_confirmation_number(self):
243
+ """Verify pickup filtering by confirmation_number."""
244
+ response = self.query(
245
+ """
246
+ query get_pickups($filter: PickupFilter) {
247
+ pickups(filter: $filter) {
248
+ edges {
249
+ node {
250
+ id
251
+ confirmation_number
252
+ }
253
+ }
254
+ }
255
+ }
256
+ """,
257
+ operation_name="get_pickups",
258
+ variables={"filter": {"confirmation_number": "00110215"}},
259
+ )
260
+
261
+ self.assertResponseNoErrors(response)
262
+ self.assertEqual(len(response.data["data"]["pickups"]["edges"]), 1)
263
+
264
+ def test_query_pickups_filter_by_date_range(self):
265
+ """Verify pickup filtering by date range."""
266
+ response = self.query(
267
+ """
268
+ query get_pickups($filter: PickupFilter) {
269
+ pickups(filter: $filter) {
270
+ edges {
271
+ node {
272
+ id
273
+ pickup_date
274
+ }
275
+ }
276
+ }
277
+ }
278
+ """,
279
+ operation_name="get_pickups",
280
+ variables={
281
+ "filter": {
282
+ "pickup_date_after": "2020-10-01",
283
+ "pickup_date_before": "2020-10-31",
284
+ }
285
+ },
286
+ )
287
+
288
+ self.assertResponseNoErrors(response)
289
+ self.assertEqual(len(response.data["data"]["pickups"]["edges"]), 1)
290
+
291
+ def test_query_pickups_filter_by_date_range_no_match(self):
292
+ """Verify pickup filtering with date range outside returns empty."""
293
+ response = self.query(
294
+ """
295
+ query get_pickups($filter: PickupFilter) {
296
+ pickups(filter: $filter) {
297
+ edges {
298
+ node {
299
+ id
300
+ pickup_date
301
+ }
302
+ }
303
+ }
304
+ }
305
+ """,
306
+ operation_name="get_pickups",
307
+ variables={
308
+ "filter": {
309
+ "pickup_date_after": "2021-01-01",
310
+ "pickup_date_before": "2021-12-31",
311
+ }
312
+ },
313
+ )
314
+
315
+ self.assertResponseNoErrors(response)
316
+ self.assertEqual(len(response.data["data"]["pickups"]["edges"]), 0)
317
+
318
+ def test_query_pickup_not_found(self):
319
+ """Verify querying non-existent pickup returns null."""
320
+ response = self.query(
321
+ """
322
+ query get_pickup($id: String!) {
323
+ pickup(id: $id) {
324
+ id
325
+ }
326
+ }
327
+ """,
328
+ operation_name="get_pickup",
329
+ variables={"id": "pck_nonexistent"},
330
+ )
331
+
332
+ self.assertResponseNoErrors(response)
333
+ self.assertIsNone(response.data["data"]["pickup"])
@@ -124,7 +124,6 @@ class TestRateSheets(GraphTestCase):
124
124
  )
125
125
  response_data = response.data
126
126
 
127
- print(response) # Debug print as per AGENTS.md guidelines
128
127
  self.assertResponseNoErrors(response)
129
128
  self.assertDictEqual(
130
129
  lib.to_dict(response_data, clear_empty=False),