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.
@@ -1,6 +1,8 @@
1
1
  import typing
2
2
  import datetime
3
3
  import strawberry
4
+ from itertools import groupby
5
+ from operator import itemgetter
4
6
  import django.db.models as models
5
7
  import django.db.models.functions as functions
6
8
  from django.conf import settings
@@ -173,6 +175,180 @@ class WorkspaceConfigType:
173
175
 
174
176
  # endregion
175
177
 
178
+ # ─────────────────────────────────────────────────────────────────
179
+ # Printing Options - Labels (format uses default_label_type above)
180
+ # ─────────────────────────────────────────────────────────────────
181
+ # region
182
+
183
+ @strawberry.field
184
+ def print_label_size(
185
+ self: auth.WorkspaceConfig,
186
+ ) -> typing.Optional[utils.LabelSizeEnum]:
187
+ return self.config.get("print_label_size")
188
+
189
+ @strawberry.field
190
+ def print_label_show_options(
191
+ self: auth.WorkspaceConfig,
192
+ ) -> typing.Optional[bool]:
193
+ return self.config.get("print_label_show_options")
194
+
195
+ # endregion
196
+
197
+ # ─────────────────────────────────────────────────────────────────
198
+ # Printing Options - Return Labels
199
+ # ─────────────────────────────────────────────────────────────────
200
+ # region
201
+
202
+ @strawberry.field
203
+ def print_return_label_size(
204
+ self: auth.WorkspaceConfig,
205
+ ) -> typing.Optional[utils.LabelSizeEnum]:
206
+ return self.config.get("print_return_label_size")
207
+
208
+ @strawberry.field
209
+ def print_return_label_show_options(
210
+ self: auth.WorkspaceConfig,
211
+ ) -> typing.Optional[bool]:
212
+ return self.config.get("print_return_label_show_options")
213
+
214
+ # endregion
215
+
216
+ # ─────────────────────────────────────────────────────────────────
217
+ # Printing Options - Customs Documents
218
+ # ─────────────────────────────────────────────────────────────────
219
+ # region
220
+
221
+ @strawberry.field
222
+ def print_customs_size(
223
+ self: auth.WorkspaceConfig,
224
+ ) -> typing.Optional[utils.LabelSizeEnum]:
225
+ return self.config.get("print_customs_size")
226
+
227
+ @strawberry.field
228
+ def print_customs_show_options(
229
+ self: auth.WorkspaceConfig,
230
+ ) -> typing.Optional[bool]:
231
+ return self.config.get("print_customs_show_options")
232
+
233
+ @strawberry.field
234
+ def print_customs_with_label(
235
+ self: auth.WorkspaceConfig,
236
+ ) -> typing.Optional[bool]:
237
+ return self.config.get("print_customs_with_label")
238
+
239
+ @strawberry.field
240
+ def print_customs_copies(
241
+ self: auth.WorkspaceConfig,
242
+ ) -> typing.Optional[int]:
243
+ return self.config.get("print_customs_copies")
244
+
245
+ # endregion
246
+
247
+ # ─────────────────────────────────────────────────────────────────
248
+ # Shipping Defaults - Settings
249
+ # ─────────────────────────────────────────────────────────────────
250
+ # region
251
+
252
+ @strawberry.field
253
+ def default_parcel_weight(
254
+ self: auth.WorkspaceConfig,
255
+ ) -> typing.Optional[float]:
256
+ return self.config.get("default_parcel_weight")
257
+
258
+ @strawberry.field
259
+ def default_shipping_service(
260
+ self: auth.WorkspaceConfig,
261
+ ) -> typing.Optional[str]:
262
+ return self.config.get("default_shipping_service")
263
+
264
+ @strawberry.field
265
+ def default_shipping_carrier(
266
+ self: auth.WorkspaceConfig,
267
+ ) -> typing.Optional[str]:
268
+ return self.config.get("default_shipping_carrier")
269
+
270
+ @strawberry.field
271
+ def default_export_reason(
272
+ self: auth.WorkspaceConfig,
273
+ ) -> typing.Optional[utils.ExportReasonEnum]:
274
+ return self.config.get("default_export_reason")
275
+
276
+ @strawberry.field
277
+ def default_delivery_instructions(
278
+ self: auth.WorkspaceConfig,
279
+ ) -> typing.Optional[str]:
280
+ return self.config.get("default_delivery_instructions")
281
+
282
+ # endregion
283
+
284
+ # ─────────────────────────────────────────────────────────────────
285
+ # Shipping Defaults - Label Options
286
+ # ─────────────────────────────────────────────────────────────────
287
+ # region
288
+
289
+ @strawberry.field
290
+ def label_show_postage_paid_logo(
291
+ self: auth.WorkspaceConfig,
292
+ ) -> typing.Optional[bool]:
293
+ return self.config.get("label_show_postage_paid_logo")
294
+
295
+ @strawberry.field
296
+ def label_show_qr_code(
297
+ self: auth.WorkspaceConfig,
298
+ ) -> typing.Optional[bool]:
299
+ return self.config.get("label_show_qr_code")
300
+
301
+ @strawberry.field
302
+ def customs_use_order_as_invoice(
303
+ self: auth.WorkspaceConfig,
304
+ ) -> typing.Optional[bool]:
305
+ return self.config.get("customs_use_order_as_invoice")
306
+
307
+ # endregion
308
+
309
+ # ─────────────────────────────────────────────────────────────────
310
+ # Shipping Defaults - Recommendations Preferences
311
+ # ─────────────────────────────────────────────────────────────────
312
+ # region
313
+
314
+ @strawberry.field
315
+ def pref_first_mile(
316
+ self: auth.WorkspaceConfig,
317
+ ) -> typing.Optional[typing.List[utils.FirstMileEnum]]:
318
+ return self.config.get("pref_first_mile")
319
+
320
+ @strawberry.field
321
+ def pref_last_mile(
322
+ self: auth.WorkspaceConfig,
323
+ ) -> typing.Optional[typing.List[utils.LastMileEnum]]:
324
+ return self.config.get("pref_last_mile")
325
+
326
+ @strawberry.field
327
+ def pref_form_factor(
328
+ self: auth.WorkspaceConfig,
329
+ ) -> typing.Optional[typing.List[utils.FormFactorEnum]]:
330
+ return self.config.get("pref_form_factor")
331
+
332
+ @strawberry.field
333
+ def pref_age_check(
334
+ self: auth.WorkspaceConfig,
335
+ ) -> typing.Optional[utils.AgeCheckEnum]:
336
+ return self.config.get("pref_age_check")
337
+
338
+ @strawberry.field
339
+ def pref_signature_required(
340
+ self: auth.WorkspaceConfig,
341
+ ) -> typing.Optional[bool]:
342
+ return self.config.get("pref_signature_required")
343
+
344
+ @strawberry.field
345
+ def pref_max_lead_time_days(
346
+ self: auth.WorkspaceConfig,
347
+ ) -> typing.Optional[int]:
348
+ return self.config.get("pref_max_lead_time_days")
349
+
350
+ # endregion
351
+
176
352
  @staticmethod
177
353
  @utils.authentication_required
178
354
  def resolve(info) -> typing.Optional["WorkspaceConfigType"]:
@@ -238,8 +414,14 @@ class SystemUsageType:
238
414
  .annotate(count=models.Count("id"))
239
415
  .order_by("-date")
240
416
  )
241
- order_volumes = (
242
- order_filters.OrderFilters(
417
+ # Calculate order volumes from JSONField (line_items is embedded JSON)
418
+ _compute_total = lambda items: sum(
419
+ float(i.get("value_amount") or 0) * float(i.get("quantity") or 1)
420
+ for i in (items or [])
421
+ )
422
+ orders_with_totals = [
423
+ (o["date"], _compute_total(o.get("line_items")))
424
+ for o in order_filters.OrderFilters(
243
425
  dict(
244
426
  created_before=_filter["date_before"],
245
427
  created_after=_filter["date_after"],
@@ -249,15 +431,13 @@ class SystemUsageType:
249
431
  ),
250
432
  )
251
433
  .qs.annotate(date=functions.TruncDay("created_at"))
252
- .values("date")
253
- .annotate(
254
- count=models.Sum(
255
- models.F("line_items__value_amount")
256
- * models.F("line_items__quantity")
257
- )
258
- )
434
+ .values("date", "line_items")
259
435
  .order_by("-date")
260
- )
436
+ ]
437
+ order_volumes = [
438
+ {"date": date, "count": sum(t for _, t in items)}
439
+ for date, items in groupby(orders_with_totals, key=itemgetter(0))
440
+ ]
261
441
  shipment_count = (
262
442
  filters.ShipmentFilters(
263
443
  dict(
@@ -371,12 +551,19 @@ class SystemUsageType:
371
551
 
372
552
  @strawberry.type
373
553
  class MetafieldType:
374
- object_type: str
375
554
  id: str
376
555
  key: str
377
556
  is_required: bool
378
557
  type: utils.MetafieldTypeEnum
379
558
  value: typing.Optional[utils.JSON] = None
559
+ object_id: typing.Optional[str] = None
560
+
561
+ @strawberry.field
562
+ def object_type(self: core.Metafield) -> str:
563
+ """Return the model name of the attached object, or 'metafield' if not attached."""
564
+ if self.content_type:
565
+ return self.content_type.model
566
+ return "metafield"
380
567
 
381
568
  @strawberry.field
382
569
  def parsed_value(self: core.Metafield) -> typing.Optional[utils.JSON]:
@@ -394,6 +581,8 @@ class MetafieldType:
394
581
  info,
395
582
  filter: typing.Optional[inputs.MetafieldFilter] = strawberry.UNSET,
396
583
  ) -> utils.Connection["MetafieldType"]:
584
+ from django.contrib.contenttypes.models import ContentType
585
+
397
586
  _filter = filter if not utils.is_unset(filter) else inputs.MetafieldFilter()
398
587
  queryset = core.Metafield.access_by(info.context.request)
399
588
 
@@ -404,6 +593,14 @@ class MetafieldType:
404
593
  queryset = queryset.filter(type=_filter.type)
405
594
  if not utils.is_unset(_filter.is_required):
406
595
  queryset = queryset.filter(is_required=_filter.is_required)
596
+ if not utils.is_unset(_filter.object_type):
597
+ ct = ContentType.objects.filter(model=_filter.object_type).first()
598
+ if ct:
599
+ queryset = queryset.filter(content_type=ct)
600
+ else:
601
+ queryset = queryset.none()
602
+ if not utils.is_unset(_filter.object_id):
603
+ queryset = queryset.filter(object_id=_filter.object_id)
407
604
 
408
605
  return utils.paginated_connection(queryset, **_filter.pagination())
409
606
 
@@ -452,12 +649,11 @@ class LogType:
452
649
  if User.objects.filter(
453
650
  id=info.context.request.user.id, is_staff=False
454
651
  ).exists():
455
- # exclude system carriers records if user is not staff
456
- system_carriers = [
457
- item["id"]
458
- for item in providers.Carrier.system_carriers.all().values("id")
459
- ]
460
- queryset = queryset.exclude(meta__carrier_account_id__in=system_carriers)
652
+ # exclude system connection records if user is not staff
653
+ system_connection_ids = list(
654
+ providers.SystemConnection.objects.values_list("id", flat=True)
655
+ )
656
+ queryset = queryset.exclude(meta__carrier_account_id__in=system_connection_ids)
461
657
 
462
658
  return queryset
463
659
 
@@ -534,6 +730,7 @@ class TokenType:
534
730
 
535
731
  @strawberry.field
536
732
  def permissions(self: auth.Token, info) -> typing.Optional[typing.List[str]]:
733
+ # self is a Token model instance, permissions is a @property on the model
537
734
  return self.permissions
538
735
 
539
736
  @staticmethod
@@ -555,6 +752,7 @@ class APIKeyType:
555
752
 
556
753
  @strawberry.field
557
754
  def permissions(self: auth.Token, info) -> typing.Optional[typing.List[str]]:
755
+ # self is a Token model instance, permissions is a @property on the model
558
756
  return self.permissions
559
757
 
560
758
  @staticmethod
@@ -643,29 +841,194 @@ class RateType:
643
841
 
644
842
  @strawberry.type
645
843
  class CommodityType:
646
- id: str
647
- object_type: str
648
- weight: float
649
- quantity: int
650
- metadata: utils.JSON
651
- sku: typing.Optional[str]
652
- title: typing.Optional[str]
653
- hs_code: typing.Optional[str]
654
- description: typing.Optional[str]
655
- value_amount: typing.Optional[float]
656
- weight_unit: typing.Optional[utils.WeightUnitEnum]
657
- origin_country: typing.Optional[utils.CountryCodeEnum]
658
- value_currency: typing.Optional[utils.CurrencyCodeEnum]
659
- created_at: typing.Optional[datetime.datetime]
660
- updated_at: typing.Optional[datetime.datetime]
661
- created_by: typing.Optional[UserType]
844
+ id: typing.Optional[str] = None
845
+ object_type: typing.Optional[str] = None
846
+ weight: typing.Optional[float] = None
847
+ quantity: typing.Optional[int] = None
848
+ metadata: typing.Optional[utils.JSON] = None
849
+ sku: typing.Optional[str] = None
850
+ title: typing.Optional[str] = None
851
+ hs_code: typing.Optional[str] = None
852
+ description: typing.Optional[str] = None
853
+ value_amount: typing.Optional[float] = None
854
+ weight_unit: typing.Optional[utils.WeightUnitEnum] = None
855
+ origin_country: typing.Optional[utils.CountryCodeEnum] = None
856
+ value_currency: typing.Optional[utils.CurrencyCodeEnum] = None
857
+ created_at: typing.Optional[datetime.datetime] = None
858
+ updated_at: typing.Optional[datetime.datetime] = None
859
+ created_by: typing.Optional[UserType] = None
662
860
  parent_id: typing.Optional[str] = None
663
861
  parent: typing.Optional["CommodityType"] = None
664
862
  unfulfilled_quantity: typing.Optional[int] = None
665
863
 
864
+ @staticmethod
865
+ def parse(item: dict) -> typing.Optional["CommodityType"]:
866
+ if not item:
867
+ return None
868
+ return CommodityType(
869
+ **{
870
+ "object_type": item.get("object_type", "commodity"),
871
+ "weight": item.get("weight") or 0,
872
+ "quantity": item.get("quantity") or 1,
873
+ "metadata": item.get("metadata") or {},
874
+ **{k: v for k, v in item.items() if k in CommodityType.__annotations__},
875
+ }
876
+ )
877
+
666
878
 
667
879
  @strawberry.type
668
880
  class AddressType:
881
+ id: typing.Optional[str] = None
882
+ object_type: typing.Optional[str] = None
883
+ postal_code: typing.Optional[str] = None
884
+ city: typing.Optional[str] = None
885
+ federal_tax_id: typing.Optional[str] = None
886
+ state_tax_id: typing.Optional[str] = None
887
+ person_name: typing.Optional[str] = None
888
+ company_name: typing.Optional[str] = None
889
+ country_code: typing.Optional[utils.CountryCodeEnum] = None
890
+ email: typing.Optional[str] = None
891
+ phone_number: typing.Optional[str] = None
892
+ state_code: typing.Optional[str] = None
893
+ residential: typing.Optional[bool] = None
894
+ street_number: typing.Optional[str] = None
895
+ address_line1: typing.Optional[str] = None
896
+ address_line2: typing.Optional[str] = None
897
+ created_at: typing.Optional[datetime.datetime] = None
898
+ updated_at: typing.Optional[datetime.datetime] = None
899
+ created_by: typing.Optional[UserType] = None
900
+ validate_location: typing.Optional[bool] = None
901
+ validation: typing.Optional[utils.JSON] = None
902
+
903
+ @staticmethod
904
+ def parse(address: dict) -> typing.Optional["AddressType"]:
905
+ if not address:
906
+ return None
907
+ return AddressType(
908
+ **{
909
+ "object_type": address.get("object_type", "address"),
910
+ **{k: v for k, v in address.items() if k in AddressType.__annotations__},
911
+ }
912
+ )
913
+
914
+
915
+ @strawberry.type
916
+ class ParcelType:
917
+ id: typing.Optional[str] = None
918
+ object_type: typing.Optional[str] = None
919
+ weight: typing.Optional[float] = None
920
+ width: typing.Optional[float] = None
921
+ height: typing.Optional[float] = None
922
+ length: typing.Optional[float] = None
923
+ packaging_type: typing.Optional[str] = None
924
+ package_preset: typing.Optional[str] = None
925
+ description: typing.Optional[str] = None
926
+ content: typing.Optional[str] = None
927
+ is_document: typing.Optional[bool] = None
928
+ weight_unit: typing.Optional[utils.WeightUnitEnum] = None
929
+ dimension_unit: typing.Optional[utils.DimensionUnitEnum] = None
930
+ freight_class: typing.Optional[str] = None
931
+ reference_number: typing.Optional[str] = None
932
+ created_at: typing.Optional[datetime.datetime] = None
933
+ updated_at: typing.Optional[datetime.datetime] = None
934
+ created_by: typing.Optional[UserType] = None
935
+ items: typing.Optional[typing.List[CommodityType]] = None
936
+
937
+ @staticmethod
938
+ def parse(parcel: dict) -> typing.Optional["ParcelType"]:
939
+ if not parcel:
940
+ return None
941
+ return ParcelType(
942
+ **{
943
+ "object_type": parcel.get("object_type", "parcel"),
944
+ **{k: v for k, v in parcel.items() if k in ParcelType.__annotations__ and k != "items"},
945
+ "items": [CommodityType.parse(i) for i in (parcel.get("items") or [])],
946
+ }
947
+ )
948
+
949
+
950
+ @strawberry.type
951
+ class DutyType:
952
+ paid_by: typing.Optional[utils.PaidByEnum] = None
953
+ currency: typing.Optional[utils.CurrencyCodeEnum] = None
954
+ account_number: typing.Optional[str] = None
955
+ declared_value: typing.Optional[float] = None
956
+ bill_to: typing.Optional[AddressType] = None
957
+
958
+ @staticmethod
959
+ def parse(duty: dict) -> typing.Optional["DutyType"]:
960
+ if not duty:
961
+ return None
962
+ return DutyType(
963
+ paid_by=duty.get("paid_by"),
964
+ currency=duty.get("currency"),
965
+ account_number=duty.get("account_number"),
966
+ declared_value=duty.get("declared_value"),
967
+ bill_to=AddressType.parse(duty.get("bill_to")),
968
+ )
969
+
970
+
971
+ @strawberry.type
972
+ class CustomsType:
973
+ """Customs type for embedded JSON customs data on shipments.
974
+
975
+ This is a pure data type that parses customs JSON data, not tied to a database model.
976
+ """
977
+ certify: typing.Optional[bool] = None
978
+ commercial_invoice: typing.Optional[bool] = None
979
+ content_type: typing.Optional[utils.CustomsContentTypeEnum] = None
980
+ content_description: typing.Optional[str] = None
981
+ incoterm: typing.Optional[utils.IncotermCodeEnum] = None
982
+ invoice: typing.Optional[str] = None
983
+ invoice_date: typing.Optional[str] = None
984
+ signer: typing.Optional[str] = None
985
+ options: typing.Optional[utils.JSON] = None
986
+ # Private fields for parsed nested objects
987
+ _duty: strawberry.Private[typing.Optional[DutyType]] = None
988
+ _duty_billing_address: strawberry.Private[typing.Optional[AddressType]] = None
989
+ _commodities: strawberry.Private[typing.Optional[typing.List[CommodityType]]] = None
990
+
991
+ @strawberry.field
992
+ def duty(self) -> typing.Optional[DutyType]:
993
+ return self._duty
994
+
995
+ @strawberry.field
996
+ def duty_billing_address(self) -> typing.Optional[AddressType]:
997
+ return self._duty_billing_address
998
+
999
+ @strawberry.field
1000
+ def commodities(self) -> typing.Optional[typing.List[CommodityType]]:
1001
+ return self._commodities
1002
+
1003
+ @staticmethod
1004
+ def parse(customs: dict) -> typing.Optional["CustomsType"]:
1005
+ if not customs:
1006
+ return None
1007
+ return CustomsType(
1008
+ certify=customs.get("certify"),
1009
+ commercial_invoice=customs.get("commercial_invoice"),
1010
+ content_type=customs.get("content_type"),
1011
+ content_description=customs.get("content_description"),
1012
+ incoterm=customs.get("incoterm"),
1013
+ invoice=customs.get("invoice"),
1014
+ invoice_date=customs.get("invoice_date"),
1015
+ signer=customs.get("signer"),
1016
+ options=customs.get("options"),
1017
+ _duty=DutyType.parse(customs.get("duty")),
1018
+ _duty_billing_address=AddressType.parse(customs.get("duty_billing_address")),
1019
+ _commodities=[CommodityType.parse(c) for c in (customs.get("commodities") or [])],
1020
+ )
1021
+
1022
+
1023
+ @strawberry.type
1024
+ class AddressTemplateType:
1025
+ """Address template type for reusable address templates.
1026
+
1027
+ Uses the Address model directly with meta.label for template metadata,
1028
+ following the PRD pattern for direct model templates.
1029
+ All fields are resolved from the Django model's properties.
1030
+ """
1031
+
669
1032
  id: str
670
1033
  object_type: str
671
1034
  postal_code: typing.Optional[str]
@@ -674,7 +1037,7 @@ class AddressType:
674
1037
  state_tax_id: typing.Optional[str]
675
1038
  person_name: typing.Optional[str]
676
1039
  company_name: typing.Optional[str]
677
- country_code: utils.CountryCodeEnum
1040
+ country_code: typing.Optional[utils.CountryCodeEnum]
678
1041
  email: typing.Optional[str]
679
1042
  phone_number: typing.Optional[str]
680
1043
  state_code: typing.Optional[str]
@@ -686,11 +1049,81 @@ class AddressType:
686
1049
  updated_at: typing.Optional[datetime.datetime]
687
1050
  created_by: typing.Optional[UserType]
688
1051
  validate_location: typing.Optional[bool]
689
- validation: typing.Optional[utils.JSON] = None
1052
+ validation: typing.Optional[utils.JSON]
1053
+ meta: typing.Optional[utils.JSON] = None
1054
+
1055
+
1056
+ # Standalone resolver functions for saved addresses (AddressTemplateType)
1057
+ @utils.authentication_required
1058
+ def resolve_addresses(
1059
+ info,
1060
+ filter: typing.Optional[inputs.AddressFilter] = strawberry.UNSET,
1061
+ ) -> utils.Connection[AddressTemplateType]:
1062
+ """Resolver for listing saved addresses."""
1063
+ _filter = inputs.AddressFilter() if utils.is_unset(filter) else filter
1064
+ _search = _filter.to_dict()
1065
+ _query = models.Q(meta__label__isnull=False) & ~models.Q(meta__label="")
1066
+
1067
+ if any(_search.get("label") or ""):
1068
+ _value = _search.get("label")
1069
+ _query = _query & models.Q(meta__label__icontains=_value)
1070
+
1071
+ if any(_search.get("address") or ""):
1072
+ _value = _search.get("address")
1073
+ _query = _query & (
1074
+ models.Q(address_line1__icontains=_value)
1075
+ | models.Q(address_line2__icontains=_value)
1076
+ | models.Q(postal_code__icontains=_value)
1077
+ | models.Q(person_name__icontains=_value)
1078
+ | models.Q(company_name__icontains=_value)
1079
+ | models.Q(country_code__icontains=_value)
1080
+ | models.Q(city__icontains=_value)
1081
+ | models.Q(email__icontains=_value)
1082
+ | models.Q(phone_number__icontains=_value)
1083
+ )
1084
+
1085
+ if any(_search.get("keyword") or ""):
1086
+ _value = _search.get("keyword")
1087
+ _query = _query & (
1088
+ models.Q(meta__label__icontains=_value)
1089
+ | models.Q(address_line1__icontains=_value)
1090
+ | models.Q(address_line2__icontains=_value)
1091
+ | models.Q(postal_code__icontains=_value)
1092
+ | models.Q(person_name__icontains=_value)
1093
+ | models.Q(company_name__icontains=_value)
1094
+ | models.Q(country_code__icontains=_value)
1095
+ | models.Q(city__icontains=_value)
1096
+ | models.Q(email__icontains=_value)
1097
+ | models.Q(phone_number__icontains=_value)
1098
+ )
1099
+
1100
+ if any(_search.get("usage") or ""):
1101
+ _value = _search.get("usage")
1102
+ _query = _query & models.Q(meta__usage__contains=_value)
1103
+
1104
+ queryset = manager.Address.access_by(info.context.request).filter(_query)
1105
+
1106
+ return utils.paginated_connection(queryset, **_filter.pagination())
1107
+
1108
+
1109
+ @utils.authentication_required
1110
+ def resolve_address(info, id: str) -> typing.Optional[AddressTemplateType]:
1111
+ """Resolver for getting a single saved address by ID."""
1112
+ return manager.Address.access_by(info.context.request).filter(
1113
+ id=id,
1114
+ meta__label__isnull=False,
1115
+ ).first()
690
1116
 
691
1117
 
692
1118
  @strawberry.type
693
- class ParcelType:
1119
+ class ParcelTemplateType:
1120
+ """Parcel template type for reusable parcel templates.
1121
+
1122
+ Uses the Parcel model directly with meta.label for template metadata,
1123
+ following the PRD pattern for direct model templates.
1124
+ All fields are resolved from the Django model's properties.
1125
+ """
1126
+
694
1127
  id: str
695
1128
  object_type: str
696
1129
  weight: typing.Optional[float]
@@ -706,193 +1139,187 @@ class ParcelType:
706
1139
  dimension_unit: typing.Optional[utils.DimensionUnitEnum]
707
1140
  freight_class: typing.Optional[str]
708
1141
  reference_number: typing.Optional[str]
709
- created_at: datetime.datetime
710
- updated_at: datetime.datetime
711
- created_by: UserType
1142
+ created_at: typing.Optional[datetime.datetime]
1143
+ updated_at: typing.Optional[datetime.datetime]
1144
+ created_by: typing.Optional[UserType]
1145
+ meta: typing.Optional[utils.JSON] = None
712
1146
 
713
1147
  @strawberry.field
714
- def items(self: manager.Parcel) -> typing.List[CommodityType]:
715
- return self.items.all()
1148
+ def items(self: manager.Parcel) -> typing.Optional[typing.List[CommodityType]]:
1149
+ """Items in the parcel."""
1150
+ items_rel = getattr(self, "items", None)
1151
+ if items_rel is None:
1152
+ return None
1153
+ if hasattr(items_rel, "all"):
1154
+ return list(items_rel.all())
1155
+ return items_rel
716
1156
 
717
1157
 
718
- @strawberry.type
719
- class DutyType:
720
- paid_by: typing.Optional[utils.PaidByEnum] = None
721
- currency: typing.Optional[utils.CurrencyCodeEnum] = None
722
- account_number: typing.Optional[str] = None
723
- declared_value: typing.Optional[float] = None
724
- bill_to: typing.Optional[AddressType] = None
725
-
1158
+ # Standalone resolver functions for saved parcels (ParcelTemplateType)
1159
+ @utils.authentication_required
1160
+ def resolve_parcels(
1161
+ info,
1162
+ filter: typing.Optional[inputs.TemplateFilter] = strawberry.UNSET,
1163
+ ) -> utils.Connection[ParcelTemplateType]:
1164
+ """Resolver for listing saved parcels."""
1165
+ _filter = inputs.TemplateFilter() if filter == strawberry.UNSET else filter
1166
+ _search = _filter.to_dict()
1167
+ _query = models.Q(meta__label__isnull=False) & ~models.Q(meta__label="")
726
1168
 
727
- @strawberry.type
728
- class CustomsType:
729
- id: str
730
- object_type: str
731
- certify: typing.Optional[bool] = strawberry.UNSET
732
- commercial_invoice: typing.Optional[bool] = strawberry.UNSET
733
- content_type: typing.Optional[utils.CustomsContentTypeEnum] = strawberry.UNSET
734
- content_description: typing.Optional[str] = strawberry.UNSET
735
- incoterm: typing.Optional[utils.IncotermCodeEnum] = strawberry.UNSET
736
- invoice: typing.Optional[str] = strawberry.UNSET
737
- invoice_date: typing.Optional[str] = strawberry.UNSET
738
- signer: typing.Optional[str] = strawberry.UNSET
739
- created_at: typing.Optional[datetime.datetime] = strawberry.UNSET
740
- updated_at: typing.Optional[datetime.datetime] = strawberry.UNSET
741
- created_by: typing.Optional[UserType] = strawberry.UNSET
742
- options: typing.Optional[utils.JSON] = strawberry.UNSET
743
- duty_billing_address: typing.Optional[AddressType] = strawberry.UNSET
744
-
745
- @strawberry.field
746
- def duty(self: manager) -> typing.Optional[DutyType]:
747
- if self.duty is None:
748
- return None
1169
+ if any(_search.get("label") or ""):
1170
+ _value = _search.get("label")
1171
+ _query = _query & models.Q(meta__label__icontains=_value)
749
1172
 
750
- return DutyType(**self.duty)
1173
+ if any(_search.get("keyword") or ""):
1174
+ _value = _search.get("keyword")
1175
+ _query = _query & models.Q(meta__label__icontains=_value)
751
1176
 
752
- @strawberry.field
753
- def commodities(self: manager.Customs) -> typing.List[CommodityType]:
754
- return self.commodities.all()
1177
+ if any(_search.get("usage") or ""):
1178
+ _value = _search.get("usage")
1179
+ _query = _query & models.Q(meta__usage__contains=_value)
755
1180
 
1181
+ queryset = manager.Parcel.access_by(info.context.request).filter(_query)
756
1182
 
757
- @strawberry.type
758
- class AddressTemplateType:
759
- id: str
760
- object_type: str
761
- label: str
762
- address: AddressType
763
- is_default: typing.Optional[bool] = None
1183
+ return utils.paginated_connection(queryset, **_filter.pagination())
764
1184
 
765
- @staticmethod
766
- @utils.authentication_required
767
- def resolve_list(
768
- info,
769
- filter: typing.Optional[inputs.AddressFilter] = strawberry.UNSET,
770
- ) -> utils.Connection["AddressTemplateType"]:
771
- _filter = inputs.AddressFilter() if utils.is_unset(filter) else filter
772
- _search = _filter.to_dict()
773
- _query = models.Q()
774
-
775
- if any(_search.get("label") or ""):
776
- _value = _search.get("label")
777
- _query = _query | models.Q(label__icontains=_value)
778
-
779
- if any(_search.get("address") or ""):
780
- _value = _search.get("address")
781
- _query = (
782
- _query
783
- | models.Q(address__address_line1__icontains=_value)
784
- | models.Q(address__address_line2__icontains=_value)
785
- | models.Q(address__postal_code__icontains=_value)
786
- | models.Q(address__person_name__icontains=_value)
787
- | models.Q(address__company_name__icontains=_value)
788
- | models.Q(address__country_code__icontains=_value)
789
- | models.Q(address__city__icontains=_value)
790
- | models.Q(address__email__icontains=_value)
791
- | models.Q(address__phone_number__icontains=_value)
792
- )
793
1185
 
794
- if any(_search.get("keyword") or ""):
795
- _value = _search.get("keyword")
796
- _query = (
797
- _query
798
- | models.Q(label__icontains=_value)
799
- | models.Q(address__address_line1__icontains=_value)
800
- | models.Q(address__address_line2__icontains=_value)
801
- | models.Q(address__postal_code__icontains=_value)
802
- | models.Q(address__person_name__icontains=_value)
803
- | models.Q(address__company_name__icontains=_value)
804
- | models.Q(address__country_code__icontains=_value)
805
- | models.Q(address__city__icontains=_value)
806
- | models.Q(address__email__icontains=_value)
807
- | models.Q(address__phone_number__icontains=_value)
808
- )
1186
+ @utils.authentication_required
1187
+ def resolve_parcel(info, id: str) -> typing.Optional[ParcelTemplateType]:
1188
+ """Resolver for getting a single saved parcel by ID."""
1189
+ return manager.Parcel.access_by(info.context.request).filter(
1190
+ id=id,
1191
+ meta__label__isnull=False,
1192
+ ).first()
809
1193
 
810
- _queryset = graph.Template.access_by(info.context.request).filter(
811
- _query, address__isnull=False
812
- )
813
1194
 
814
- return utils.paginated_connection(_queryset, **_filter.pagination())
1195
+ @strawberry.type
1196
+ class ProductTemplateType:
1197
+ """Product template type for reusable product/commodity templates.
815
1198
 
1199
+ Uses the Commodity model directly with meta.label for template metadata,
1200
+ following the PRD pattern for direct model templates.
1201
+ All fields are resolved from the Django model's properties.
1202
+ """
816
1203
 
817
- @strawberry.type
818
- class ParcelTemplateType:
819
1204
  id: str
820
1205
  object_type: str
821
- label: str
822
- parcel: ParcelType
823
- is_default: typing.Optional[bool]
1206
+ weight: float
1207
+ quantity: int
1208
+ sku: typing.Optional[str]
1209
+ title: typing.Optional[str]
1210
+ hs_code: typing.Optional[str]
1211
+ description: typing.Optional[str]
1212
+ value_amount: typing.Optional[float]
1213
+ weight_unit: typing.Optional[utils.WeightUnitEnum]
1214
+ origin_country: typing.Optional[utils.CountryCodeEnum]
1215
+ value_currency: typing.Optional[utils.CurrencyCodeEnum]
1216
+ created_at: typing.Optional[datetime.datetime]
1217
+ updated_at: typing.Optional[datetime.datetime]
1218
+ created_by: typing.Optional[UserType]
1219
+ metadata: typing.Optional[utils.JSON] = None
1220
+ meta: typing.Optional[utils.JSON] = None
824
1221
 
825
- @staticmethod
826
- @utils.authentication_required
827
- def resolve_list(
828
- info,
829
- filter: typing.Optional[inputs.TemplateFilter] = strawberry.UNSET,
830
- ) -> utils.Connection["ParcelTemplateType"]:
831
- _filter = inputs.TemplateFilter() if filter == strawberry.UNSET else filter
832
- _search = _filter.to_dict()
833
- _query = models.Q()
834
-
835
- if any(_search.get("label") or ""):
836
- _value = _search.get("label")
837
- _query = _query | models.Q(label__icontains=_value)
838
-
839
- if any(_search.get("keyword") or ""):
840
- _value = _search.get("keyword")
841
- _query = _query | models.Q(label__icontains=_value)
842
-
843
- queryset = graph.Template.access_by(info.context.request).filter(
844
- _query,
845
- parcel__isnull=False,
1222
+
1223
+ # Standalone resolver functions for saved products (ProductTemplateType)
1224
+ @utils.authentication_required
1225
+ def resolve_products(
1226
+ info,
1227
+ filter: typing.Optional[inputs.ProductFilter] = strawberry.UNSET,
1228
+ ) -> utils.Connection[ProductTemplateType]:
1229
+ """Resolver for listing saved products."""
1230
+ _filter = filter if not utils.is_unset(filter) else inputs.ProductFilter()
1231
+ _search = _filter.to_dict()
1232
+ _query = models.Q(meta__label__isnull=False) & ~models.Q(meta__label="")
1233
+
1234
+ if any(_search.get("label") or ""):
1235
+ _value = _search.get("label")
1236
+ _query = _query & models.Q(meta__label__icontains=_value)
1237
+
1238
+ if any(_search.get("keyword") or ""):
1239
+ _value = _search.get("keyword")
1240
+ _query = _query & (
1241
+ models.Q(meta__label__icontains=_value)
1242
+ | models.Q(title__icontains=_value)
1243
+ | models.Q(sku__icontains=_value)
1244
+ | models.Q(description__icontains=_value)
1245
+ | models.Q(hs_code__icontains=_value)
846
1246
  )
847
1247
 
848
- return utils.paginated_connection(queryset, **_filter.pagination())
1248
+ if any(_search.get("sku") or ""):
1249
+ _value = _search.get("sku")
1250
+ _query = _query & models.Q(sku__icontains=_value)
849
1251
 
1252
+ if _search.get("origin_country"):
1253
+ _query = _query & models.Q(origin_country=_search.get("origin_country"))
850
1254
 
851
- @strawberry.type
852
- class CustomsTemplateType:
853
- id: str
854
- object_type: str
855
- label: str
856
- customs: CustomsType
857
- is_default: typing.Optional[bool]
1255
+ if any(_search.get("usage") or ""):
1256
+ _value = _search.get("usage")
1257
+ _query = _query & models.Q(meta__usage__contains=_value)
858
1258
 
859
- @staticmethod
860
- @utils.authentication_required
861
- def resolve_list(
862
- info,
863
- filter: typing.Optional[inputs.TemplateFilter] = strawberry.UNSET,
864
- ) -> utils.Connection["CustomsTemplateType"]:
865
- _filter = filter if not utils.is_unset(filter) else inputs.TemplateFilter()
1259
+ queryset = manager.Commodity.access_by(info.context.request).filter(_query)
866
1260
 
867
- queryset = graph.Template.access_by(info.context.request).filter(
868
- customs__isnull=False,
869
- **(
870
- {"label__icontain": _filter.label}
871
- if _filter.label is not strawberry.UNSET
872
- else {}
873
- ),
874
- )
875
- return utils.paginated_connection(queryset, **_filter.pagination())
1261
+ return utils.paginated_connection(queryset, **_filter.pagination())
1262
+
1263
+
1264
+ @utils.authentication_required
1265
+ def resolve_product(info, id: str) -> typing.Optional[ProductTemplateType]:
1266
+ """Resolver for getting a single saved product by ID."""
1267
+ return manager.Commodity.access_by(info.context.request).filter(
1268
+ id=id,
1269
+ meta__label__isnull=False,
1270
+ ).first()
876
1271
 
877
1272
 
878
1273
  @strawberry.type
879
1274
  class DefaultTemplatesType:
880
- default_address: typing.Optional[AddressTemplateType] = None
881
- default_customs: typing.Optional[CustomsTemplateType] = None
882
- default_parcel: typing.Optional[ParcelTemplateType] = None
1275
+ """Default templates type for user's default address, parcel, and product templates.
883
1276
 
884
- @staticmethod
885
- @utils.authentication_required
886
- def resolve(info) -> "DefaultTemplatesType":
887
- templates = graph.Template.access_by(info.context.request).filter(
888
- is_default=True
889
- )
1277
+ Uses strawberry.Private to store the actual model instances and @strawberry.field
1278
+ methods to return them as the correct strawberry types, ensuring proper field resolution.
1279
+ """
890
1280
 
891
- return DefaultTemplatesType( # type: ignore
892
- default_address=templates.filter(address__isnull=False).first(),
893
- default_customs=templates.filter(customs__isnull=False).first(),
894
- default_parcel=templates.filter(parcel__isnull=False).first(),
895
- )
1281
+ _default_address: strawberry.Private[typing.Optional[manager.Address]] = None
1282
+ _default_parcel: strawberry.Private[typing.Optional[manager.Parcel]] = None
1283
+ _default_product: strawberry.Private[typing.Optional[manager.Commodity]] = None
1284
+
1285
+ @strawberry.field
1286
+ def default_address(self) -> typing.Optional[AddressTemplateType]:
1287
+ """Returns the default address template."""
1288
+ return self._default_address # type: ignore
1289
+
1290
+ @strawberry.field
1291
+ def default_parcel(self) -> typing.Optional[ParcelTemplateType]:
1292
+ """Returns the default parcel template."""
1293
+ return self._default_parcel # type: ignore
1294
+
1295
+ @strawberry.field
1296
+ def default_product(self) -> typing.Optional[ProductTemplateType]:
1297
+ """Returns the default product template."""
1298
+ return self._default_product # type: ignore
1299
+
1300
+
1301
+ # Standalone resolver function for DefaultTemplatesType
1302
+ @utils.authentication_required
1303
+ def resolve_default_templates(info) -> DefaultTemplatesType:
1304
+ """Resolver for getting default templates."""
1305
+ default_address = manager.Address.access_by(info.context.request).filter(
1306
+ meta__is_default=True,
1307
+ meta__label__isnull=False,
1308
+ ).first()
1309
+ default_parcel = manager.Parcel.access_by(info.context.request).filter(
1310
+ meta__is_default=True,
1311
+ meta__label__isnull=False,
1312
+ ).first()
1313
+ default_product = manager.Commodity.access_by(info.context.request).filter(
1314
+ meta__is_default=True,
1315
+ meta__label__isnull=False,
1316
+ ).first()
1317
+
1318
+ return DefaultTemplatesType(
1319
+ _default_address=default_address,
1320
+ _default_parcel=default_parcel,
1321
+ _default_product=default_product,
1322
+ )
896
1323
 
897
1324
 
898
1325
  @strawberry.type
@@ -945,6 +1372,33 @@ class TrackingInfoType:
945
1372
  )
946
1373
 
947
1374
 
1375
+ @strawberry.type
1376
+ class CarrierSnapshotType:
1377
+ """Represents a carrier snapshot stored at the time of operation."""
1378
+
1379
+ connection_id: typing.Optional[str] = None
1380
+ connection_type: typing.Optional[str] = None
1381
+ carrier_code: typing.Optional[str] = None
1382
+ carrier_id: typing.Optional[str] = None
1383
+ carrier_name: typing.Optional[str] = None
1384
+ test_mode: typing.Optional[bool] = None
1385
+
1386
+ @staticmethod
1387
+ def parse(
1388
+ snapshot: typing.Optional[dict],
1389
+ ) -> typing.Optional["CarrierSnapshotType"]:
1390
+ if not snapshot:
1391
+ return None
1392
+ return CarrierSnapshotType(
1393
+ connection_id=snapshot.get("connection_id"),
1394
+ connection_type=snapshot.get("connection_type"),
1395
+ carrier_code=snapshot.get("carrier_code"),
1396
+ carrier_id=snapshot.get("carrier_id"),
1397
+ carrier_name=snapshot.get("carrier_name"),
1398
+ test_mode=snapshot.get("test_mode"),
1399
+ )
1400
+
1401
+
948
1402
  @strawberry.type
949
1403
  class TrackerType:
950
1404
  id: str
@@ -965,12 +1419,12 @@ class TrackerType:
965
1419
  created_by: UserType
966
1420
 
967
1421
  @strawberry.field
968
- def carrier_id(self: manager.Tracking) -> str:
969
- return getattr(self.tracking_carrier, "carrier_id", None)
1422
+ def carrier_id(self: manager.Tracking) -> typing.Optional[str]:
1423
+ return (self.carrier or {}).get("carrier_id")
970
1424
 
971
1425
  @strawberry.field
972
- def carrier_name(self: manager.Tracking) -> str:
973
- return getattr(self.tracking_carrier, "carrier_name", None)
1426
+ def carrier_name(self: manager.Tracking) -> typing.Optional[str]:
1427
+ return (self.carrier or {}).get("carrier_name")
974
1428
 
975
1429
  @strawberry.field
976
1430
  def info(self: manager.Tracking) -> typing.Optional[TrackingInfoType]:
@@ -987,8 +1441,8 @@ class TrackerType:
987
1441
  @strawberry.field
988
1442
  def tracking_carrier(
989
1443
  self: manager.Tracking,
990
- ) -> typing.Optional["CarrierConnectionType"]:
991
- return self.tracking_carrier
1444
+ ) -> typing.Optional[CarrierSnapshotType]:
1445
+ return CarrierSnapshotType.parse(self.carrier)
992
1446
 
993
1447
  @staticmethod
994
1448
  @utils.authentication_required
@@ -1024,12 +1478,12 @@ class ManifestType:
1024
1478
  updated_at: datetime.datetime
1025
1479
 
1026
1480
  @strawberry.field
1027
- def carrier_id(self: manager.Manifest) -> str:
1028
- return getattr(self.manifest_carrier, "carrier_id", None)
1481
+ def carrier_id(self: manager.Manifest) -> typing.Optional[str]:
1482
+ return (self.carrier or {}).get("carrier_id")
1029
1483
 
1030
1484
  @strawberry.field
1031
- def carrier_name(self: manager.Manifest) -> str:
1032
- return getattr(self.manifest_carrier, "carrier_name", None)
1485
+ def carrier_name(self: manager.Manifest) -> typing.Optional[str]:
1486
+ return (self.carrier or {}).get("carrier_name")
1033
1487
 
1034
1488
  @strawberry.field
1035
1489
  def messages(self: manager.Manifest) -> typing.List[MessageType]:
@@ -1038,8 +1492,8 @@ class ManifestType:
1038
1492
  @strawberry.field
1039
1493
  def manifest_carrier(
1040
1494
  self: manager.Manifest,
1041
- ) -> typing.Optional["CarrierConnectionType"]:
1042
- return self.manifest_carrier
1495
+ ) -> typing.Optional[CarrierSnapshotType]:
1496
+ return CarrierSnapshotType.parse(self.carrier)
1043
1497
 
1044
1498
  @staticmethod
1045
1499
  @utils.authentication_required
@@ -1059,6 +1513,100 @@ class ManifestType:
1059
1513
  return utils.paginated_connection(queryset, **_filter.pagination())
1060
1514
 
1061
1515
 
1516
+ @strawberry.type
1517
+ class PickupType:
1518
+ """GraphQL type for Pickup model."""
1519
+
1520
+ id: str
1521
+ object_type: str
1522
+ confirmation_number: typing.Optional[str]
1523
+ pickup_date: typing.Optional[str]
1524
+ ready_time: typing.Optional[str]
1525
+ closing_time: typing.Optional[str]
1526
+ instruction: typing.Optional[str]
1527
+ package_location: typing.Optional[str]
1528
+ test_mode: bool
1529
+ options: utils.JSON
1530
+ metadata: utils.JSON
1531
+ meta: typing.Optional[utils.JSON]
1532
+ created_at: datetime.datetime
1533
+ updated_at: datetime.datetime
1534
+ created_by: UserType
1535
+
1536
+ @strawberry.field
1537
+ def pickup_type(self: manager.Pickup) -> str:
1538
+ """Pickup scheduling type: one_time, daily, or recurring."""
1539
+ return self.pickup_type or "one_time"
1540
+
1541
+ @strawberry.field
1542
+ def recurrence(self: manager.Pickup) -> typing.Optional[utils.JSON]:
1543
+ """Recurrence config for recurring pickups."""
1544
+ return self.recurrence
1545
+
1546
+ @strawberry.field
1547
+ def carrier_id(self: manager.Pickup) -> typing.Optional[str]:
1548
+ return (self.carrier or {}).get("carrier_id")
1549
+
1550
+ @strawberry.field
1551
+ def carrier_name(self: manager.Pickup) -> typing.Optional[str]:
1552
+ return (self.carrier or {}).get("carrier_name")
1553
+
1554
+ @strawberry.field
1555
+ def address(self: manager.Pickup) -> typing.Optional[AddressType]:
1556
+ return AddressType.parse(self.address) if self.address else None
1557
+
1558
+ @strawberry.field
1559
+ def pickup_charge(self: manager.Pickup) -> typing.Optional[ChargeType]:
1560
+ return ChargeType.parse(self.pickup_charge) if self.pickup_charge else None
1561
+
1562
+ @strawberry.field
1563
+ def parcels(self: manager.Pickup) -> typing.List[ParcelType]:
1564
+ """Parcels from related shipments."""
1565
+ return [
1566
+ ParcelType.parse(p)
1567
+ for shipment in self.shipments.all()
1568
+ for p in (shipment.parcels or [])
1569
+ ]
1570
+
1571
+ @strawberry.field
1572
+ def tracking_numbers(self: manager.Pickup) -> typing.List[str]:
1573
+ """Tracking numbers from related shipments."""
1574
+ return [
1575
+ s.tracking_number
1576
+ for s in self.shipments.all()
1577
+ if s.tracking_number
1578
+ ]
1579
+
1580
+ @strawberry.field
1581
+ def shipments(self: manager.Pickup) -> typing.List["ShipmentType"]:
1582
+ """Related shipments for this pickup."""
1583
+ return list(self.shipments.all())
1584
+
1585
+ @strawberry.field
1586
+ def pickup_carrier(
1587
+ self: manager.Pickup,
1588
+ ) -> typing.Optional[CarrierSnapshotType]:
1589
+ """Carrier snapshot with credentials protected."""
1590
+ return CarrierSnapshotType.parse(self.carrier)
1591
+
1592
+ @staticmethod
1593
+ @utils.authentication_required
1594
+ def resolve(info, id: str) -> typing.Optional["PickupType"]:
1595
+ return manager.Pickup.access_by(info.context.request).filter(id=id).first()
1596
+
1597
+ @staticmethod
1598
+ @utils.authentication_required
1599
+ def resolve_list(
1600
+ info,
1601
+ filter: typing.Optional[inputs.PickupFilter] = strawberry.UNSET,
1602
+ ) -> utils.Connection["PickupType"]:
1603
+ _filter = filter if not utils.is_unset(filter) else inputs.PickupFilter()
1604
+ queryset = filters.PickupFilters(
1605
+ _filter.to_dict(), manager.Pickup.access_by(info.context.request)
1606
+ ).qs
1607
+ return utils.paginated_connection(queryset, **_filter.pagination())
1608
+
1609
+
1062
1610
  @strawberry.type
1063
1611
  class PaymentType:
1064
1612
  account_number: typing.Optional[str] = None
@@ -1070,23 +1618,17 @@ class ShipmentType:
1070
1618
  id: str
1071
1619
  object_type: str
1072
1620
  test_mode: bool
1073
- shipper: AddressType
1074
- recipient: AddressType
1075
1621
  options: utils.JSON
1076
1622
  metadata: utils.JSON
1077
1623
  status: utils.ShipmentStatusEnum
1078
- return_address: typing.Optional[AddressType]
1079
- billing_address: typing.Optional[AddressType]
1080
1624
  meta: typing.Optional[utils.JSON]
1081
1625
  label_type: typing.Optional[utils.LabelTypeEnum]
1082
1626
  tracking_number: typing.Optional[str]
1083
1627
  shipment_identifier: typing.Optional[str]
1084
1628
  tracking_url: typing.Optional[str]
1085
1629
  reference: typing.Optional[str]
1086
- customs: typing.Optional[CustomsType]
1087
1630
  services: typing.Optional[typing.List[str]]
1088
1631
  service: typing.Optional[str]
1089
- carrier_ids: typing.List[str]
1090
1632
  selected_rate_id: typing.Optional[str]
1091
1633
  tracker_id: typing.Optional[str]
1092
1634
  label_url: typing.Optional[str]
@@ -1096,17 +1638,46 @@ class ShipmentType:
1096
1638
  updated_at: datetime.datetime
1097
1639
  created_by: UserType
1098
1640
 
1641
+ @strawberry.field
1642
+ def shipper(self: manager.Shipment) -> AddressType:
1643
+ return AddressType.parse(self.shipper)
1644
+
1645
+ @strawberry.field
1646
+ def recipient(self: manager.Shipment) -> AddressType:
1647
+ return AddressType.parse(self.recipient)
1648
+
1649
+ @strawberry.field
1650
+ def return_address(self: manager.Shipment) -> typing.Optional[AddressType]:
1651
+ return AddressType.parse(self.return_address)
1652
+
1653
+ @strawberry.field
1654
+ def billing_address(self: manager.Shipment) -> typing.Optional[AddressType]:
1655
+ return AddressType.parse(self.billing_address)
1656
+
1657
+ @strawberry.field
1658
+ def customs(self: manager.Shipment) -> typing.Optional[CustomsType]:
1659
+ return CustomsType.parse(self.customs)
1660
+
1099
1661
  @strawberry.field
1100
1662
  def carrier_id(self: manager.Shipment) -> typing.Optional[str]:
1101
- return getattr(self.selected_rate_carrier, "carrier_id", None)
1663
+ if self.selected_rate is None:
1664
+ return None
1665
+ return self.selected_rate.get("carrier_id")
1102
1666
 
1103
1667
  @strawberry.field
1104
1668
  def carrier_name(self: manager.Shipment) -> typing.Optional[str]:
1105
- return getattr(self.selected_rate_carrier, "carrier_name", None)
1669
+ if self.selected_rate is None:
1670
+ return None
1671
+ return self.selected_rate.get("carrier_name")
1672
+
1673
+ @strawberry.field
1674
+ def carrier_ids(self: manager.Shipment) -> typing.List[str]:
1675
+ return self.carrier_ids or []
1106
1676
 
1107
1677
  @strawberry.field
1108
1678
  def parcels(self: manager.Shipment) -> typing.List[ParcelType]:
1109
- return self.parcels.all()
1679
+ # parcels is now a JSON field, return parsed ParcelType objects
1680
+ return [ParcelType.parse(p) for p in (self.parcels or [])]
1110
1681
 
1111
1682
  @strawberry.field
1112
1683
  def rates(self: manager.Shipment) -> typing.List[RateType]:
@@ -1119,8 +1690,10 @@ class ShipmentType:
1119
1690
  @strawberry.field
1120
1691
  def selected_rate_carrier(
1121
1692
  self: manager.Shipment,
1122
- ) -> typing.Optional["CarrierConnectionType"]:
1123
- return self.selected_rate_carrier
1693
+ ) -> typing.Optional[CarrierSnapshotType]:
1694
+ if self.carrier is None:
1695
+ return None
1696
+ return CarrierSnapshotType.parse(self.carrier)
1124
1697
 
1125
1698
  @strawberry.field
1126
1699
  def payment(self: manager.Shipment) -> typing.Optional[PaymentType]:
@@ -1261,6 +1834,74 @@ class ServiceRateType:
1261
1834
  )
1262
1835
 
1263
1836
 
1837
+ @strawberry.type
1838
+ class ServiceLevelFeaturesType:
1839
+ """Structured service level features.
1840
+
1841
+ Defines the capabilities and characteristics of a shipping service.
1842
+ Used for filtering, display, and setting default options.
1843
+ """
1844
+
1845
+ # First Mile: How parcels get to the carrier
1846
+ # "pick_up" | "drop_off" | "pick_up_and_drop_off"
1847
+ first_mile: typing.Optional[str] = None
1848
+
1849
+ # Last Mile: How parcels are delivered to recipient
1850
+ # "home_delivery" | "service_point" | "mailbox"
1851
+ last_mile: typing.Optional[str] = None
1852
+
1853
+ # Form Factor: Type of package the service supports
1854
+ # "letter" | "parcel" | "mailbox" | "pallet"
1855
+ form_factor: typing.Optional[str] = None
1856
+
1857
+ # Type of Shipments: Business model support
1858
+ b2c: typing.Optional[bool] = None # Business to Consumer
1859
+ b2b: typing.Optional[bool] = None # Business to Business
1860
+
1861
+ # Shipment Direction: "outbound" | "returns" | "both"
1862
+ shipment_type: typing.Optional[str] = None
1863
+
1864
+ # Age Verification: null | "16" | "18"
1865
+ age_check: typing.Optional[str] = None
1866
+
1867
+ # Default signature requirement
1868
+ signature: typing.Optional[bool] = None
1869
+
1870
+ # Tracking availability
1871
+ tracked: typing.Optional[bool] = None
1872
+
1873
+ # Insurance availability
1874
+ insurance: typing.Optional[bool] = None
1875
+
1876
+ # Express/Priority service
1877
+ express: typing.Optional[bool] = None
1878
+
1879
+ # Dangerous goods support
1880
+ dangerous_goods: typing.Optional[bool] = None
1881
+
1882
+ # Weekend delivery options
1883
+ saturday_delivery: typing.Optional[bool] = None
1884
+ sunday_delivery: typing.Optional[bool] = None
1885
+
1886
+ # Multi-package shipment support
1887
+ multicollo: typing.Optional[bool] = None
1888
+
1889
+ # Neighbor delivery allowed
1890
+ neighbor_delivery: typing.Optional[bool] = None
1891
+
1892
+ @staticmethod
1893
+ def parse(features: typing.Optional[dict]) -> "ServiceLevelFeaturesType":
1894
+ """Parse a features dict into ServiceLevelFeaturesType."""
1895
+ if not features or not isinstance(features, dict):
1896
+ return ServiceLevelFeaturesType()
1897
+
1898
+ import dataclasses
1899
+ field_names = {f.name for f in dataclasses.fields(ServiceLevelFeaturesType)}
1900
+ return ServiceLevelFeaturesType(**{
1901
+ k: v for k, v in features.items() if k in field_names
1902
+ })
1903
+
1904
+
1264
1905
  @strawberry.type
1265
1906
  class ServiceLevelType:
1266
1907
  """
@@ -1294,9 +1935,18 @@ class ServiceLevelType:
1294
1935
  max_volume: typing.Optional[float]
1295
1936
  cost: typing.Optional[float]
1296
1937
 
1938
+ # Volumetric weight fields
1939
+ dim_factor: typing.Optional[float]
1940
+ use_volumetric: typing.Optional[bool]
1941
+
1297
1942
  domicile: typing.Optional[bool]
1298
1943
  international: typing.Optional[bool]
1299
1944
 
1945
+ @strawberry.field
1946
+ def features(self: providers.ServiceLevel) -> ServiceLevelFeaturesType:
1947
+ """Structured service features."""
1948
+ return ServiceLevelFeaturesType.parse(self.features)
1949
+
1300
1950
  @strawberry.field
1301
1951
  def metadata(self: providers.ServiceLevel) -> typing.Optional[utils.JSON]:
1302
1952
  try:
@@ -1335,6 +1985,10 @@ class RateSheetType:
1335
1985
  slug: str
1336
1986
  carrier_name: utils.CarrierNameEnum
1337
1987
 
1988
+ @strawberry.field
1989
+ def origin_countries(self: providers.RateSheet) -> typing.Optional[typing.List[str]]:
1990
+ return self.origin_countries or []
1991
+
1338
1992
  @strawberry.field
1339
1993
  def metadata(self: providers.RateSheet) -> typing.Optional[utils.JSON]:
1340
1994
  try:
@@ -1401,29 +2055,61 @@ class RateSheetType:
1401
2055
 
1402
2056
  @strawberry.type
1403
2057
  class SystemConnectionType:
2058
+ """Represents a SystemConnection that can be enabled by users via BrokeredConnection."""
2059
+
1404
2060
  id: str
1405
- active: bool
1406
2061
  carrier_id: str
2062
+ carrier_code: str
1407
2063
  display_name: str
1408
2064
  test_mode: bool
1409
2065
  capabilities: typing.List[str]
1410
2066
  created_at: typing.Optional[datetime.datetime]
1411
2067
  updated_at: typing.Optional[datetime.datetime]
1412
2068
 
1413
- @strawberry.field
1414
- def carrier_name(self: providers.Carrier) -> str:
1415
- return getattr(self, "settings", self).carrier_name
1416
-
1417
- @strawberry.field
1418
- def enabled(self: providers.Carrier, info: Info) -> bool:
1419
- if hasattr(self, "active_orgs"):
1420
- return self.active_orgs.filter(id=info.context.request.org.id).exists()
1421
-
1422
- return self.active_users.filter(id=info.context.request.user.id).exists()
2069
+ active: bool
1423
2070
 
1424
2071
  @strawberry.field
1425
- def config(self: providers.Carrier, info: Info) -> typing.Optional[utils.JSON]:
1426
- return getattr(self, "config", None)
2072
+ def carrier_name(self: providers.SystemConnection) -> str:
2073
+ return self.carrier_code
2074
+
2075
+ @strawberry.field
2076
+ def enabled(self: providers.SystemConnection, info: Info) -> bool:
2077
+ """Check if this SystemConnection is enabled for the current user/org."""
2078
+ if settings.MULTI_ORGANIZATIONS:
2079
+ org_id = getattr(info.context.request, "org", None)
2080
+ org_id = org_id.id if org_id else None
2081
+ return providers.BrokeredConnection.objects.filter(
2082
+ system_connection=self,
2083
+ is_enabled=True,
2084
+ link__org__id=org_id,
2085
+ ).exists()
2086
+ else:
2087
+ user_id = getattr(info.context.request, "user", None)
2088
+ user_id = user_id.id if user_id else None
2089
+ return providers.BrokeredConnection.objects.filter(
2090
+ system_connection=self,
2091
+ is_enabled=True,
2092
+ created_by__id=user_id,
2093
+ ).exists()
2094
+
2095
+ @strawberry.field
2096
+ def config(self: providers.SystemConnection, info: Info) -> typing.Optional[utils.JSON]:
2097
+ """Get the user's config for this SystemConnection from their BrokeredConnection."""
2098
+ if settings.MULTI_ORGANIZATIONS:
2099
+ org_id = getattr(info.context.request, "org", None)
2100
+ org_id = org_id.id if org_id else None
2101
+ brokered = providers.BrokeredConnection.objects.filter(
2102
+ system_connection=self,
2103
+ link__org__id=org_id,
2104
+ ).first()
2105
+ else:
2106
+ user_id = getattr(info.context.request, "user", None)
2107
+ user_id = user_id.id if user_id else None
2108
+ brokered = providers.BrokeredConnection.objects.filter(
2109
+ system_connection=self,
2110
+ created_by__id=user_id,
2111
+ ).first()
2112
+ return brokered.config if brokered else None
1427
2113
 
1428
2114
  @staticmethod
1429
2115
  @utils.authentication_required
@@ -1432,42 +2118,46 @@ class SystemConnectionType:
1432
2118
  filter: typing.Optional[inputs.CarrierFilter] = strawberry.UNSET,
1433
2119
  ) -> utils.Connection["SystemConnectionType"]:
1434
2120
  _filter = filter if not utils.is_unset(filter) else inputs.CarrierFilter()
1435
- connections = filters.CarrierFilters(
1436
- _filter.to_dict(),
1437
- providers.Carrier.system_carriers.resolve_config_for(
1438
- info.context.request
1439
- ).filter(
1440
- active=True,
1441
- test_mode=getattr(info.context.request, "test_mode", False),
1442
- ),
1443
- ).qs
1444
- return utils.paginated_connection(connections, **_filter.pagination())
2121
+ queryset = providers.SystemConnection.objects.filter(
2122
+ active=True,
2123
+ test_mode=getattr(info.context.request, "test_mode", False),
2124
+ )
2125
+ # Apply carrier filter if specified
2126
+ if _filter.carrier_name:
2127
+ queryset = queryset.filter(carrier_code=_filter.carrier_name)
2128
+ return utils.paginated_connection(queryset, **_filter.pagination())
1445
2129
 
1446
2130
 
1447
2131
  @strawberry.type
1448
2132
  class CarrierConnectionType:
2133
+ """GraphQL type for carrier connections."""
2134
+
1449
2135
  id: str
1450
2136
  carrier_id: str
2137
+ carrier_code: str
1451
2138
  carrier_name: str
1452
2139
  display_name: str
1453
2140
  active: bool
1454
- is_system: bool
1455
2141
  test_mode: bool
1456
- credentials: utils.JSON
1457
2142
  capabilities: typing.List[str]
1458
- rate_sheet: typing.Optional[RateSheetType] = None
1459
2143
 
1460
2144
  @strawberry.field
1461
- def metadata(self: providers.Carrier, info: Info) -> typing.Optional[utils.JSON]:
2145
+ def credentials(self: providers.CarrierConnection, info: Info) -> utils.JSON:
2146
+ return self.credentials
2147
+
2148
+ @strawberry.field
2149
+ def metadata(self: providers.CarrierConnection, info: Info) -> typing.Optional[utils.JSON]:
1462
2150
  return getattr(self, "metadata", None)
1463
2151
 
1464
2152
  @strawberry.field
1465
- def config(self: providers.Carrier, info: Info) -> typing.Optional[utils.JSON]:
2153
+ def config(self: providers.CarrierConnection, info: Info) -> typing.Optional[utils.JSON]:
1466
2154
  return getattr(self, "config", None)
1467
2155
 
2156
+ @strawberry.field
1468
2157
  def rate_sheet(
1469
- self: providers.Carrier, info: Info
2158
+ self: providers.CarrierConnection, info: Info
1470
2159
  ) -> typing.Optional[RateSheetType]:
2160
+ # Access rate_sheet FK from the Django model
1471
2161
  return getattr(self, "rate_sheet", None)
1472
2162
 
1473
2163
  @staticmethod
@@ -1478,9 +2168,10 @@ class CarrierConnectionType:
1478
2168
  filter: typing.Optional[inputs.CarrierFilter] = strawberry.UNSET,
1479
2169
  ) -> typing.List["CarrierConnectionType"]:
1480
2170
  _filter = filter if not utils.is_unset(filter) else inputs.CarrierFilter()
2171
+ # Carrier model now only contains user/org-owned connections (no is_system filter needed)
1481
2172
  connections = filters.CarrierFilters(
1482
2173
  _filter.to_dict(),
1483
- providers.Carrier.access_by(info.context.request).filter(is_system=False),
2174
+ providers.CarrierConnection.access_by(info.context.request),
1484
2175
  ).qs
1485
2176
  return connections
1486
2177
 
@@ -1492,7 +2183,7 @@ class CarrierConnectionType:
1492
2183
  id: str,
1493
2184
  ) -> typing.Optional["CarrierConnectionType"]:
1494
2185
  connection = (
1495
- providers.Carrier.access_by(info.context.request).filter(id=id).first()
2186
+ providers.CarrierConnection.access_by(info.context.request).filter(id=id).first()
1496
2187
  )
1497
2188
  return connection
1498
2189
 
@@ -1504,9 +2195,10 @@ class CarrierConnectionType:
1504
2195
  filter: typing.Optional[inputs.CarrierFilter] = strawberry.UNSET,
1505
2196
  ) -> utils.Connection["CarrierConnectionType"]:
1506
2197
  _filter = filter if not utils.is_unset(filter) else inputs.CarrierFilter()
2198
+ # Carrier model now only contains user/org-owned connections (no is_system filter needed)
1507
2199
  queryset = filters.CarrierFilters(
1508
2200
  _filter.to_dict(),
1509
- providers.Carrier.access_by(info.context.request).filter(is_system=False),
2201
+ providers.CarrierConnection.access_by(info.context.request),
1510
2202
  ).qs
1511
2203
  connections = utils.paginated_connection(queryset, **_filter.pagination())
1512
2204