karrio-server-core 2025.5__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 (213) hide show
  1. karrio/server/conf.py +54 -0
  2. karrio/server/core/__init__.py +3 -0
  3. karrio/server/core/admin.py +1 -0
  4. karrio/server/core/apps.py +10 -0
  5. karrio/server/core/authentication.py +347 -0
  6. karrio/server/core/config.py +31 -0
  7. karrio/server/core/context_processors.py +12 -0
  8. karrio/server/core/datatypes.py +394 -0
  9. karrio/server/core/dataunits.py +187 -0
  10. karrio/server/core/exceptions.py +404 -0
  11. karrio/server/core/fields.py +12 -0
  12. karrio/server/core/filters.py +837 -0
  13. karrio/server/core/gateway.py +1011 -0
  14. karrio/server/core/logging.py +403 -0
  15. karrio/server/core/management/commands/cli.py +19 -0
  16. karrio/server/core/management/commands/create_oauth_client.py +41 -0
  17. karrio/server/core/management/commands/runserver.py +5 -0
  18. karrio/server/core/middleware.py +197 -0
  19. karrio/server/core/migrations/0001_initial.py +28 -0
  20. karrio/server/core/migrations/0002_apilogindex.py +69 -0
  21. karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
  22. karrio/server/core/migrations/0004_metafield.py +74 -0
  23. karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
  24. karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
  25. karrio/server/core/migrations/__init__.py +0 -0
  26. karrio/server/core/models/__init__.py +48 -0
  27. karrio/server/core/models/base.py +103 -0
  28. karrio/server/core/models/entity.py +24 -0
  29. karrio/server/core/models/metafield.py +144 -0
  30. karrio/server/core/models/third_party.py +21 -0
  31. karrio/server/core/oauth_validators.py +170 -0
  32. karrio/server/core/permissions.py +36 -0
  33. karrio/server/core/renderers.py +11 -0
  34. karrio/server/core/router.py +3 -0
  35. karrio/server/core/serializers.py +1971 -0
  36. karrio/server/core/signals.py +55 -0
  37. karrio/server/core/telemetry.py +573 -0
  38. karrio/server/core/tests.py +99 -0
  39. karrio/server/core/tests_resource_token.py +411 -0
  40. karrio/server/core/urls.py +12 -0
  41. karrio/server/core/utils.py +1025 -0
  42. karrio/server/core/validators.py +264 -0
  43. karrio/server/core/views/__init__.py +2 -0
  44. karrio/server/core/views/api.py +133 -0
  45. karrio/server/core/views/metadata.py +44 -0
  46. karrio/server/core/views/oauth.py +75 -0
  47. karrio/server/core/views/references.py +82 -0
  48. karrio/server/core/views/schema.py +310 -0
  49. karrio/server/filters/__init__.py +2 -0
  50. karrio/server/filters/abstract.py +26 -0
  51. karrio/server/iam/__init__.py +0 -0
  52. karrio/server/iam/admin.py +3 -0
  53. karrio/server/iam/apps.py +21 -0
  54. karrio/server/iam/migrations/0001_initial.py +33 -0
  55. karrio/server/iam/migrations/__init__.py +0 -0
  56. karrio/server/iam/models.py +48 -0
  57. karrio/server/iam/permissions.py +155 -0
  58. karrio/server/iam/serializers.py +54 -0
  59. karrio/server/iam/signals.py +18 -0
  60. karrio/server/iam/tests.py +3 -0
  61. karrio/server/iam/views.py +3 -0
  62. karrio/server/openapi.py +75 -0
  63. karrio/server/providers/__init__.py +1 -0
  64. karrio/server/providers/admin.py +364 -0
  65. karrio/server/providers/apps.py +10 -0
  66. karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
  67. karrio/server/providers/migrations/0001_initial.py +140 -0
  68. karrio/server/providers/migrations/0002_carrier_active.py +18 -0
  69. karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
  70. karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
  71. karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
  72. karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
  73. karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
  74. karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
  75. karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
  76. karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
  77. karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
  78. karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
  79. karrio/server/providers/migrations/0013_tntsettings.py +30 -0
  80. karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
  81. karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
  82. karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
  83. karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
  84. karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
  85. karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
  86. karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
  87. karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
  88. karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
  89. karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
  90. karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
  91. karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
  92. karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
  93. karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
  94. karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
  95. karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
  96. karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
  97. karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
  98. karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
  99. karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
  100. karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
  101. karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
  102. karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
  103. karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
  104. karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
  105. karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
  106. karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
  107. karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
  108. karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
  109. karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
  110. karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
  111. karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
  112. karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
  113. karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
  114. karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
  115. karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
  116. karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
  117. karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
  118. karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
  119. karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
  120. karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
  121. karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
  122. karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
  123. karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
  124. karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
  125. karrio/server/providers/migrations/0059_ratesheet.py +81 -0
  126. karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
  127. karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
  128. karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
  129. karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
  130. karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
  131. karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
  132. karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
  133. karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
  134. karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
  135. karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
  136. karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
  137. karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
  138. karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
  139. karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
  140. karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
  141. karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
  142. karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
  143. karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
  144. karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
  145. karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
  146. karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
  147. karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
  148. karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
  149. karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
  150. karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
  151. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  152. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  153. karrio/server/providers/migrations/__init__.py +0 -0
  154. karrio/server/providers/models/__init__.py +16 -0
  155. karrio/server/providers/models/carrier.py +387 -0
  156. karrio/server/providers/models/config.py +30 -0
  157. karrio/server/providers/models/service.py +192 -0
  158. karrio/server/providers/models/sheet.py +287 -0
  159. karrio/server/providers/models/template.py +39 -0
  160. karrio/server/providers/models/utils.py +58 -0
  161. karrio/server/providers/router.py +3 -0
  162. karrio/server/providers/serializers/__init__.py +3 -0
  163. karrio/server/providers/serializers/base.py +538 -0
  164. karrio/server/providers/signals.py +25 -0
  165. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  166. karrio/server/providers/tests/__init__.py +5 -0
  167. karrio/server/providers/tests/test_connections.py +895 -0
  168. karrio/server/providers/urls.py +11 -0
  169. karrio/server/providers/views/__init__.py +0 -0
  170. karrio/server/providers/views/carriers.py +267 -0
  171. karrio/server/providers/views/connections.py +496 -0
  172. karrio/server/samples.py +352 -0
  173. karrio/server/serializers/__init__.py +2 -0
  174. karrio/server/serializers/abstract.py +602 -0
  175. karrio/server/tracing/__init__.py +0 -0
  176. karrio/server/tracing/admin.py +63 -0
  177. karrio/server/tracing/apps.py +8 -0
  178. karrio/server/tracing/migrations/0001_initial.py +41 -0
  179. karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
  180. karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
  181. karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
  182. karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
  183. karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
  184. karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
  185. karrio/server/tracing/migrations/__init__.py +0 -0
  186. karrio/server/tracing/models.py +82 -0
  187. karrio/server/tracing/tests.py +3 -0
  188. karrio/server/tracing/utils.py +109 -0
  189. karrio/server/user/__init__.py +0 -0
  190. karrio/server/user/admin.py +96 -0
  191. karrio/server/user/apps.py +7 -0
  192. karrio/server/user/forms.py +35 -0
  193. karrio/server/user/migrations/0001_initial.py +41 -0
  194. karrio/server/user/migrations/0002_token.py +29 -0
  195. karrio/server/user/migrations/0003_token_test_mode.py +20 -0
  196. karrio/server/user/migrations/0004_group.py +26 -0
  197. karrio/server/user/migrations/0005_token_label.py +21 -0
  198. karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
  199. karrio/server/user/migrations/0007_user_metadata.py +25 -0
  200. karrio/server/user/migrations/__init__.py +0 -0
  201. karrio/server/user/models.py +218 -0
  202. karrio/server/user/serializers.py +47 -0
  203. karrio/server/user/templates/registration/login.html +108 -0
  204. karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
  205. karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
  206. karrio/server/user/tests.py +3 -0
  207. karrio/server/user/urls.py +10 -0
  208. karrio/server/user/utils.py +60 -0
  209. karrio/server/user/views.py +9 -0
  210. karrio_server_core-2025.5.dist-info/METADATA +32 -0
  211. karrio_server_core-2025.5.dist-info/RECORD +213 -0
  212. karrio_server_core-2025.5.dist-info/WHEEL +5 -0
  213. karrio_server_core-2025.5.dist-info/top_level.txt +2 -0
@@ -0,0 +1,71 @@
1
+ # Generated by Django migration to rename customer_number to billing_number
2
+ # for DHL Parcel DE connections. This keeps existing configuration values
3
+ # when the field was renamed in the settings schema.
4
+
5
+ from django.db import migrations
6
+
7
+
8
+ def rename_customer_number_to_billing_number(apps, schema_editor):
9
+ """
10
+ Find all dhl_parcel_de carrier connections that have customer_number
11
+ and rename it to billing_number to preserve the configuration value.
12
+ """
13
+ Carrier = apps.get_model("providers", "Carrier")
14
+ db_alias = schema_editor.connection.alias
15
+
16
+ # Find all dhl_parcel_de carriers
17
+ dhl_parcel_de_carriers = Carrier.objects.using(db_alias).filter(
18
+ carrier_code="dhl_parcel_de"
19
+ )
20
+
21
+ for carrier in dhl_parcel_de_carriers:
22
+ credentials = carrier.credentials or {}
23
+
24
+ # Check if customer_number exists and billing_number doesn't
25
+ if credentials.get("customer_number") and not credentials.get("billing_number"):
26
+ # Copy customer_number value to billing_number
27
+ credentials["billing_number"] = credentials["customer_number"]
28
+ # Remove old customer_number field
29
+ del credentials["customer_number"]
30
+
31
+ carrier.credentials = credentials
32
+ carrier.save(using=db_alias, update_fields=["credentials"])
33
+
34
+
35
+ def rename_billing_number_to_customer_number(apps, schema_editor):
36
+ """
37
+ Reverse migration: rename billing_number back to customer_number.
38
+ """
39
+ Carrier = apps.get_model("providers", "Carrier")
40
+ db_alias = schema_editor.connection.alias
41
+
42
+ dhl_parcel_de_carriers = Carrier.objects.using(db_alias).filter(
43
+ carrier_code="dhl_parcel_de"
44
+ )
45
+
46
+ for carrier in dhl_parcel_de_carriers:
47
+ credentials = carrier.credentials or {}
48
+
49
+ # Check if billing_number exists and customer_number doesn't
50
+ if credentials.get("billing_number") and not credentials.get("customer_number"):
51
+ # Copy billing_number value back to customer_number
52
+ credentials["customer_number"] = credentials["billing_number"]
53
+ # Remove the billing_number field
54
+ del credentials["billing_number"]
55
+
56
+ carrier.credentials = credentials
57
+ carrier.save(using=db_alias, update_fields=["credentials"])
58
+
59
+
60
+ class Migration(migrations.Migration):
61
+
62
+ dependencies = [
63
+ ("providers", "0085_populate_dhl_parcel_de_oauth_credentials"),
64
+ ]
65
+
66
+ operations = [
67
+ migrations.RunPython(
68
+ rename_customer_number_to_billing_number,
69
+ rename_billing_number_to_customer_number,
70
+ ),
71
+ ]
File without changes
@@ -0,0 +1,16 @@
1
+
2
+ from karrio.server.providers.models.sheet import RateSheet
3
+ from karrio.server.providers.models.config import CarrierConfig
4
+ from karrio.server.providers.models.service import ServiceLevel
5
+ from karrio.server.providers.models.template import LabelTemplate
6
+ from karrio.server.providers.models.utils import has_rate_sheet
7
+ from karrio.server.core.models.base import register_model
8
+ from karrio.server.providers.models.carrier import (
9
+ Carrier,
10
+ COUNTRIES,
11
+ CURRENCIES,
12
+ WEIGHT_UNITS,
13
+ DIMENSION_UNITS,
14
+ CAPABILITIES_CHOICES,
15
+ create_carrier_proxy,
16
+ )
@@ -0,0 +1,387 @@
1
+ import typing
2
+ import functools
3
+ import django.conf as conf
4
+ import django.forms as forms
5
+ import django.db.models as models
6
+ from django.db.models import Q, OuterRef, Subquery, Case, When, IntegerField
7
+
8
+ import karrio.lib as lib
9
+ import karrio.sdk as karrio
10
+ import karrio.core.units as units
11
+ import django.core.cache as caching
12
+ import karrio.api.gateway as gateway
13
+ import karrio.server.core.models as core
14
+ import karrio.server.core.fields as fields
15
+ import karrio.server.core.datatypes as datatypes
16
+
17
+
18
+ COUNTRIES = [(c.name, c.name) for c in units.Country]
19
+ CURRENCIES = [(c.name, c.name) for c in units.Currency]
20
+ WEIGHT_UNITS = [(c.name, c.name) for c in units.WeightUnit]
21
+ DIMENSION_UNITS = [(c.name, c.name) for c in units.DimensionUnit]
22
+ CAPABILITIES_CHOICES = [(c, c) for c in units.CarrierCapabilities.get_capabilities()]
23
+
24
+
25
+ class CarrierQuerySet(models.QuerySet):
26
+ def resolve_config_for(self, context):
27
+ from karrio.server.providers.models.config import CarrierConfig
28
+
29
+ if context is None:
30
+ return self
31
+
32
+ user = getattr(context, "user", None)
33
+ org = getattr(context, "org", None)
34
+
35
+ if isinstance(context, dict):
36
+ user = user or context.get("user")
37
+ org = org or context.get("org")
38
+
39
+ # 1. Define what "My Config" looks like (User or Org specific)
40
+ my_config_filter = Q()
41
+ if org:
42
+ my_config_filter = Q(org=org)
43
+ elif user and getattr(user, "is_authenticated", False):
44
+ my_config_filter = Q(created_by=user)
45
+
46
+ # 2. Define what "System Default" looks like
47
+ system_default_filter = Q(created_by__is_staff=True)
48
+ if hasattr(CarrierConfig, "org"):
49
+ system_default_filter &= Q(org__isnull=True)
50
+
51
+ # 3. Build the Subquery - only use priority if we have a user/org filter
52
+ config_filter = (
53
+ my_config_filter | system_default_filter
54
+ if my_config_filter
55
+ else system_default_filter
56
+ )
57
+
58
+ config_query = CarrierConfig.objects.filter(carrier=OuterRef("pk")).filter(
59
+ config_filter
60
+ )
61
+
62
+ if my_config_filter:
63
+ # Prioritize user/org config over system default
64
+ config_query = config_query.annotate(
65
+ priority=Case(
66
+ When(my_config_filter, then=0),
67
+ default=1,
68
+ output_field=IntegerField(),
69
+ )
70
+ ).order_by("priority")
71
+
72
+ # 4. Annotate the queryset
73
+ return self.annotate(
74
+ _computed_config=Subquery(config_query.values("config")[:1])
75
+ )
76
+
77
+
78
+ class Manager(models.Manager):
79
+ def get_queryset(self):
80
+ return (
81
+ CarrierQuerySet(self.model, using=self._db)
82
+ .select_related(
83
+ "created_by",
84
+ "rate_sheet",
85
+ *(("link",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
86
+ )
87
+ .prefetch_related(
88
+ "active_users",
89
+ *(("org",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
90
+ )
91
+ )
92
+
93
+ def resolve_config_for(self, context):
94
+ return self.get_queryset().resolve_config_for(context)
95
+
96
+
97
+ class CarrierManager(Manager):
98
+ def get_queryset(self):
99
+ return super().get_queryset().filter(is_system=False)
100
+
101
+
102
+ class SystemCarrierManager(models.Manager):
103
+ def get_queryset(self):
104
+ return (
105
+ CarrierQuerySet(self.model, using=self._db)
106
+ .prefetch_related(
107
+ *(("active_orgs",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
108
+ )
109
+ .select_related(
110
+ "created_by",
111
+ *(("link",) if conf.settings.MULTI_ORGANIZATIONS else tuple()),
112
+ )
113
+ .filter(is_system=True)
114
+ )
115
+
116
+ def resolve_config_for(self, context):
117
+ return self.get_queryset().resolve_config_for(context)
118
+
119
+
120
+ @core.register_model
121
+ class Carrier(core.OwnedEntity):
122
+ class Meta:
123
+ ordering = ["test_mode", "-created_at"]
124
+
125
+ CONTEXT_RELATIONS = ["rate_sheet"]
126
+
127
+ objects = Manager()
128
+ user_carriers = CarrierManager()
129
+ system_carriers = SystemCarrierManager()
130
+
131
+ id = models.CharField(
132
+ max_length=50,
133
+ primary_key=True,
134
+ default=functools.partial(core.uuid, prefix="car_"),
135
+ editable=False,
136
+ )
137
+ carrier_code = models.CharField(
138
+ max_length=100,
139
+ db_index=True,
140
+ default="generic",
141
+ help_text="eg. dhl_express, fedex, ups, usps, ...",
142
+ )
143
+ carrier_id = models.CharField(
144
+ max_length=150,
145
+ db_index=True,
146
+ help_text="eg. canadapost, dhl_express, fedex, purolator_courrier, ups...",
147
+ )
148
+ credentials = models.JSONField(
149
+ default=core.field_default({}),
150
+ help_text="Carrier connection credentials",
151
+ )
152
+ capabilities = fields.MultiChoiceField(
153
+ choices=datatypes.CAPABILITIES_CHOICES,
154
+ default=core.field_default([]),
155
+ help_text="Select the capabilities of the carrier that you want to enable",
156
+ )
157
+ metadata = models.JSONField(
158
+ blank=True,
159
+ null=True,
160
+ default=core.field_default({}),
161
+ help_text="User defined metadata",
162
+ )
163
+ active = models.BooleanField(
164
+ default=True,
165
+ db_index=True,
166
+ help_text="Disable/Hide carrier from clients",
167
+ )
168
+ is_system = models.BooleanField(
169
+ default=False,
170
+ db_index=True,
171
+ help_text="Determine that the carrier connection is available system wide.",
172
+ )
173
+ test_mode = models.BooleanField(
174
+ default=True,
175
+ db_column="test_mode",
176
+ help_text="Toggle carrier connection mode",
177
+ )
178
+
179
+ created_by = models.ForeignKey(
180
+ conf.settings.AUTH_USER_MODEL,
181
+ blank=True,
182
+ null=True,
183
+ on_delete=models.CASCADE,
184
+ editable=False,
185
+ )
186
+ active_users = models.ManyToManyField(
187
+ conf.settings.AUTH_USER_MODEL,
188
+ blank=True,
189
+ related_name="connection_users",
190
+ )
191
+ rate_sheet = models.ForeignKey(
192
+ "RateSheet",
193
+ null=True,
194
+ blank=True,
195
+ on_delete=models.SET_NULL,
196
+ )
197
+
198
+ @classmethod
199
+ def resolve_context_data(cls, queryset, context):
200
+ """Apply context-aware carrier config resolution."""
201
+ return queryset.resolve_config_for(context)
202
+
203
+ def __str__(self):
204
+ return self.carrier_id
205
+
206
+ @property
207
+ def object_type(self):
208
+ return "carrier-connection"
209
+
210
+ @property
211
+ def ext(self) -> str:
212
+ return (
213
+ "generic"
214
+ if "custom_carrier_name" in self.credentials
215
+ else lib.failsafe(lambda: self.carrier_code) or "generic"
216
+ )
217
+
218
+ @property
219
+ def carrier_name(self):
220
+ return (
221
+ "generic"
222
+ if "custom_carrier_name" in self.credentials
223
+ else lib.failsafe(lambda: self.carrier_code) or "generic"
224
+ )
225
+
226
+ @property
227
+ def display_name(self):
228
+ import karrio.references as references
229
+
230
+ return (
231
+ self.credentials.get("display_name")
232
+ or references.REFERENCES["carriers"].get(self.ext)
233
+ or "generic"
234
+ )
235
+
236
+ @property
237
+ def carrier_config(self):
238
+ return self.__class__.resolve_config(self)
239
+
240
+ @property
241
+ def config(self) -> dict:
242
+ if hasattr(self, "_computed_config"):
243
+ annotated_config = self._computed_config
244
+ if annotated_config is not None:
245
+ return annotated_config
246
+ # If the annotation didn't resolve (eg. context missing), fall back to DB lookup.
247
+ resolved_config = getattr(self.carrier_config, "config", None)
248
+ if resolved_config is not None:
249
+ return resolved_config
250
+ # Return empty dict if no config is found - do NOT fallback to credentials
251
+ return {}
252
+
253
+ @property
254
+ def services(self) -> typing.Optional[typing.List[dict]]:
255
+
256
+ if self.rate_sheet is None:
257
+ return None
258
+
259
+ return self.rate_sheet.services.all()
260
+
261
+ @property
262
+ def data(self) -> datatypes.CarrierSettings:
263
+ _computed_data: typing.Dict = dict(
264
+ id=self.id,
265
+ config=self.config,
266
+ test_mode=self.test_mode,
267
+ metadata=self.metadata,
268
+ carrier_id=self.carrier_id,
269
+ carrier_name=self.ext,
270
+ display_name=self.display_name,
271
+ )
272
+
273
+ if any(self.services or []):
274
+ _computed_data.update(
275
+ services=[forms.model_to_dict(s) for s in self.services]
276
+ )
277
+
278
+ # override the config with the system config
279
+ if self.is_system and self.carrier_config is None:
280
+ _config = self.__class__.resolve_config(self, is_system_config=True)
281
+ _computed_data.update(config=getattr(_config, "config", None))
282
+
283
+ return datatypes.CarrierSettings.create(
284
+ {
285
+ **self.credentials,
286
+ **_computed_data,
287
+ }
288
+ )
289
+
290
+ @property
291
+ def gateway(self) -> gateway.Gateway:
292
+ import karrio.server.core.middleware as middleware
293
+ import karrio.server.core.config as system_config
294
+
295
+ _context = middleware.SessionContext.get_current_request()
296
+ _tracer = getattr(_context, "tracer", lib.Tracer())
297
+ _cache = lib.Cache(caching.cache)
298
+ _config = lib.SystemConfig(system_config.config)
299
+
300
+ return karrio.gateway[self.ext].create(
301
+ self.data.to_dict(),
302
+ _tracer,
303
+ _cache,
304
+ _config,
305
+ )
306
+
307
+ @staticmethod
308
+ def resolve_config(
309
+ carrier, is_user_config: bool = False, is_system_config: bool = False
310
+ ):
311
+ import karrio.server.serializers as serializers
312
+ import karrio.server.core.middleware as middleware
313
+ from django.contrib.auth.models import AnonymousUser
314
+ from karrio.server.providers.models.config import CarrierConfig
315
+
316
+ if carrier.id is None:
317
+ return None
318
+
319
+ _ctx = serializers.get_object_context(carrier)
320
+ ctx = lib.identity(
321
+ _ctx
322
+ if (_ctx.user or _ctx.org)
323
+ else lib.failsafe(lambda: middleware.SessionContext.get_current_request())
324
+ )
325
+ has_ctx_user = lib.identity(
326
+ ctx and ((ctx.user and not isinstance(ctx.user, AnonymousUser)) or ctx.org)
327
+ )
328
+
329
+ queryset = lib.identity(
330
+ CarrierConfig.objects.filter(carrier=carrier)
331
+ if carrier.is_system
332
+ else CarrierConfig.access_by(ctx).filter(carrier=carrier)
333
+ )
334
+
335
+ if carrier.is_system:
336
+ _config = queryset.filter(
337
+ created_by__is_staff=True,
338
+ **({"org": None} if hasattr(carrier, "org") else {}),
339
+ ).first()
340
+
341
+ if has_ctx_user:
342
+ return queryset.filter(
343
+ **(
344
+ {"org": (None if is_system_config else ctx.org)}
345
+ if hasattr(carrier, "org")
346
+ else {"created_by": ctx.user}
347
+ )
348
+ ).first() or (None if is_user_config else _config)
349
+
350
+ return _config
351
+
352
+ return queryset.first()
353
+
354
+
355
+ def create_carrier_proxy(carrier_name: str, display_name):
356
+ class _Manager(Manager):
357
+ def get_queryset(self):
358
+ return super().get_queryset().filter(carrier_code=carrier_name)
359
+
360
+ class _CarrierManager(CarrierManager):
361
+ def get_queryset(self):
362
+ return super().get_queryset().filter(carrier_code=carrier_name)
363
+
364
+ class _SystemCarrierManager(SystemCarrierManager):
365
+ def get_queryset(self):
366
+ return super().get_queryset().filter(carrier_code=carrier_name)
367
+
368
+ return type(
369
+ f"{carrier_name}Connection",
370
+ (Carrier,),
371
+ {
372
+ "Meta": type(
373
+ "Meta",
374
+ (),
375
+ {
376
+ "proxy": True,
377
+ "__module__": __name__,
378
+ "verbose_name": f"{display_name} Connection",
379
+ "verbose_name_plural": f"{display_name} Connections",
380
+ },
381
+ ),
382
+ "__module__": __name__,
383
+ "objects": _Manager(),
384
+ "user_carriers": _CarrierManager(),
385
+ "system_carriers": _SystemCarrierManager(),
386
+ },
387
+ )
@@ -0,0 +1,30 @@
1
+ import functools
2
+ import django.db.models as models
3
+ import karrio.server.core.models as core
4
+
5
+
6
+ @core.register_model
7
+ class CarrierConfig(core.OwnedEntity):
8
+ class Meta:
9
+ db_table = "carrier-config"
10
+ verbose_name = "Carrier Config"
11
+ verbose_name_plural = "Carrier Configs"
12
+ ordering = ["-created_at"]
13
+
14
+ id = models.CharField(
15
+ max_length=50,
16
+ editable=False,
17
+ primary_key=True,
18
+ default=functools.partial(core.uuid, prefix="cfg_"),
19
+ )
20
+ carrier = models.ForeignKey(
21
+ "Carrier",
22
+ null=False,
23
+ related_name="configs",
24
+ on_delete=models.CASCADE,
25
+ )
26
+ config = models.JSONField(
27
+ null=False,
28
+ blank=False,
29
+ default=core.field_default({}),
30
+ )
@@ -0,0 +1,192 @@
1
+ import functools
2
+ import django.db.models as models
3
+ import django.core.validators as validators
4
+
5
+ import karrio.server.core.models as core
6
+ import karrio.server.core.datatypes as datatypes
7
+
8
+
9
+ @core.register_model
10
+ class ServiceLevel(core.OwnedEntity):
11
+ class Meta:
12
+ db_table = "service-level"
13
+ verbose_name = "Service Level"
14
+ verbose_name_plural = "Service Levels"
15
+ ordering = ["-created_at"]
16
+
17
+ id = models.CharField(
18
+ max_length=50,
19
+ primary_key=True,
20
+ default=functools.partial(core.uuid, prefix="svc_"),
21
+ editable=False,
22
+ )
23
+ service_name = models.CharField(max_length=50)
24
+ service_code = models.CharField(
25
+ max_length=50, validators=[validators.RegexValidator(r"^[a-z0-9_]+$")]
26
+ )
27
+ carrier_service_code = models.CharField(max_length=50, null=True, blank=True)
28
+ description = models.CharField(max_length=250, null=True, blank=True)
29
+ active = models.BooleanField(null=True, default=True)
30
+
31
+ currency = models.CharField(
32
+ max_length=4, choices=datatypes.CURRENCIES, null=True, blank=True
33
+ )
34
+
35
+ transit_days = models.IntegerField(blank=True, null=True)
36
+ transit_time = models.FloatField(blank=True, null=True)
37
+
38
+ max_width = models.FloatField(blank=True, null=True)
39
+ max_height = models.FloatField(blank=True, null=True)
40
+ max_length = models.FloatField(blank=True, null=True)
41
+ dimension_unit = models.CharField(
42
+ max_length=2, choices=datatypes.DIMENSION_UNITS, null=True, blank=True
43
+ )
44
+
45
+ min_weight = models.FloatField(blank=True, null=True)
46
+ max_weight = models.FloatField(blank=True, null=True)
47
+ weight_unit = models.CharField(
48
+ max_length=2, choices=datatypes.WEIGHT_UNITS, null=True, blank=True
49
+ )
50
+
51
+ domicile = models.BooleanField(null=True)
52
+ international = models.BooleanField(null=True)
53
+
54
+ zones = models.JSONField(blank=True, null=True, default=core.field_default([]))
55
+ metadata = models.JSONField(blank=True, null=True, default=core.field_default({}))
56
+
57
+ def __str__(self):
58
+ return f"{self.id} | {self.service_name}"
59
+
60
+ @property
61
+ def object_type(self):
62
+ return "service_level"
63
+
64
+ @property
65
+ def computed_zones(self):
66
+ """
67
+ Computed property that returns zones in legacy format for backward compatibility.
68
+ If the service belongs to a rate sheet with optimized structure, reconstruct from there.
69
+ Otherwise, fall back to the service's own zones field.
70
+ """
71
+ # Check if this service belongs to a rate sheet with optimized structure
72
+ rate_sheet = getattr(self, '_rate_sheet_cache', None)
73
+ if not rate_sheet:
74
+ # Try to find rate sheet this service belongs to
75
+ try:
76
+ rate_sheet = self.service_sheet.first()
77
+ self._rate_sheet_cache = rate_sheet
78
+ except:
79
+ rate_sheet = None
80
+
81
+ if rate_sheet and rate_sheet.zones and rate_sheet.service_rates:
82
+ # Use optimized structure
83
+ return rate_sheet.get_service_zones_legacy(self.id)
84
+ else:
85
+ # Fall back to legacy zones field
86
+ return self.zones or []
87
+
88
+ def update_zone_cell(self, zone_id: str, field: str, value):
89
+ """Update a single field in a zone by ID or index with validation"""
90
+ # Define allowed fields with their validators
91
+ allowed_fields = {
92
+ 'rate': float,
93
+ 'min_weight': float,
94
+ 'max_weight': float,
95
+ 'transit_days': int,
96
+ 'transit_time': float,
97
+ 'label': str,
98
+ 'radius': float,
99
+ 'latitude': float,
100
+ 'longitude': float,
101
+ }
102
+
103
+ if field not in allowed_fields:
104
+ raise ValueError(f"Field '{field}' is not allowed for zone updates")
105
+
106
+ # Validate and convert the value
107
+ try:
108
+ if value is not None and value != '':
109
+ value = allowed_fields[field](value)
110
+ except (ValueError, TypeError):
111
+ raise ValueError(f"Invalid value '{value}' for field '{field}' (expected {allowed_fields[field].__name__})")
112
+
113
+ zones = self.zones or []
114
+
115
+ # First try to find by zone ID
116
+ for zone in zones:
117
+ if zone.get('id') == zone_id:
118
+ zone[field] = value
119
+ self.save(update_fields=['zones'])
120
+ return zone
121
+
122
+ # Fallback: try to find by index for zones without IDs
123
+ try:
124
+ zone_index = int(zone_id)
125
+ if 0 <= zone_index < len(zones):
126
+ zones[zone_index][field] = value
127
+ self.save(update_fields=['zones'])
128
+ return zones[zone_index]
129
+ except (ValueError, IndexError):
130
+ pass
131
+
132
+ raise ValueError(f"Zone {zone_id} not found")
133
+
134
+ def batch_update_cells(self, updates: list):
135
+ """
136
+ Batch update multiple zone cells with validation
137
+ updates format: [{'zone_id': str, 'field': str, 'value': any}, ...]
138
+ """
139
+ # Define allowed fields with their validators
140
+ allowed_fields = {
141
+ 'rate': float,
142
+ 'min_weight': float,
143
+ 'max_weight': float,
144
+ 'transit_days': int,
145
+ 'transit_time': float,
146
+ 'label': str,
147
+ 'radius': float,
148
+ 'latitude': float,
149
+ 'longitude': float,
150
+ }
151
+
152
+ zones = list(self.zones or [])
153
+
154
+ for update in updates:
155
+ zone_id = update.get('zone_id')
156
+ field = update.get('field')
157
+ value = update.get('value')
158
+
159
+ if field not in allowed_fields:
160
+ raise ValueError(f"Field '{field}' is not allowed for zone updates")
161
+
162
+ # Validate and convert the value
163
+ try:
164
+ if value is not None and value != '':
165
+ value = allowed_fields[field](value)
166
+ except (ValueError, TypeError):
167
+ raise ValueError(f"Invalid value '{value}' for field '{field}' (expected {allowed_fields[field].__name__})")
168
+
169
+ # Find zone by ID first, then by index
170
+ zone_found = False
171
+ for zone in zones:
172
+ if zone.get('id') == zone_id:
173
+ zone[field] = value
174
+ zone_found = True
175
+ break
176
+
177
+ # Fallback to index if zone_id is numeric and zone not found by ID
178
+ if not zone_found:
179
+ try:
180
+ zone_index = int(zone_id)
181
+ if 0 <= zone_index < len(zones):
182
+ zones[zone_index][field] = value
183
+ zone_found = True
184
+ except (ValueError, IndexError):
185
+ pass
186
+
187
+ if not zone_found:
188
+ raise ValueError(f"Zone {zone_id} not found")
189
+
190
+ self.zones = zones
191
+ self.save(update_fields=['zones'])
192
+ return self.zones