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,496 @@
1
+ import django.urls as urls
2
+ import rest_framework.status as status
3
+ import rest_framework.request as request
4
+ import rest_framework.response as response
5
+ import django_filters.rest_framework as drf
6
+ import rest_framework.pagination as pagination
7
+
8
+ import karrio.lib as lib
9
+ import karrio.server.openapi as openapi
10
+ import karrio.server.core.views.api as api
11
+ import karrio.server.core.filters as filters
12
+ import karrio.server.core.gateway as gateway
13
+ import karrio.server.providers.models as models
14
+ import karrio.server.providers.serializers as serializers
15
+
16
+ ENDPOINT_ID = "&&&" # This endpoint id is used to make operation ids unique make sure not to duplicate
17
+ CarrierConnectionList = serializers.PaginatedResult(
18
+ "CarrierConnectionList", serializers.CarrierConnection
19
+ )
20
+
21
+
22
+ class CarrierConnectionListView(api.GenericAPIView):
23
+ model = models.Carrier
24
+ serializer_class = serializers.CarrierConnection
25
+ filterset_class = filters.CarrierConnectionFilter
26
+ filter_backends = (drf.DjangoFilterBackend,)
27
+ pagination_class = type(
28
+ "CustomPagination",
29
+ (pagination.LimitOffsetPagination,),
30
+ dict(default_limit=20),
31
+ )
32
+
33
+ @openapi.extend_schema(
34
+ tags=["Connections"],
35
+ operation_id=f"{ENDPOINT_ID}list",
36
+ extensions={"x-operationId": "listCarrierConnections"},
37
+ summary="List carrier connections",
38
+ responses={
39
+ 200: CarrierConnectionList(),
40
+ 400: serializers.ErrorResponse(),
41
+ },
42
+ parameters=filters.CarrierConnectionFilter.parameters,
43
+ )
44
+ def get(self, request: request.Request):
45
+ """Retrieve all carrier connections"""
46
+ filter = {
47
+ **filters.CarrierFilters(request.query_params).to_dict(),
48
+ "context": request,
49
+ }
50
+
51
+ # fmt: off
52
+ connections = gateway.Carriers.list(**filter)
53
+ response = self.paginate_queryset(
54
+ [
55
+ {
56
+ **_,
57
+ "metadata": None if _["is_system"] else _["metadata"],
58
+ "credentials": None if _["is_system"] else _["credentials"]
59
+ }
60
+ for _ in serializers.CarrierConnection(connections, many=True).data
61
+ ]
62
+ )
63
+ # fmt: on
64
+
65
+ return self.get_paginated_response(response)
66
+
67
+ @openapi.extend_schema(
68
+ tags=["Connections"],
69
+ operation_id=f"{ENDPOINT_ID}add",
70
+ extensions={"x-operationId": "addCarrierConnection"},
71
+ summary="Add a carrier connection",
72
+ responses={
73
+ 201: serializers.CarrierConnection(),
74
+ 400: serializers.ErrorResponse(),
75
+ 424: serializers.ErrorMessages(),
76
+ 500: serializers.ErrorResponse(),
77
+ },
78
+ request=serializers.CarrierConnectionData(),
79
+ )
80
+ def post(self, request: request.Request):
81
+ """Add a new carrier connection."""
82
+ connection = lib.identity(
83
+ serializers.CarrierConnectionModelSerializer.map(
84
+ data=serializers.CarrierConnectionData.map(data=request.data).data,
85
+ context=request,
86
+ )
87
+ .save()
88
+ .instance
89
+ )
90
+
91
+ return response.Response(
92
+ serializers.CarrierConnection(connection).data,
93
+ status=status.HTTP_201_CREATED,
94
+ )
95
+
96
+
97
+ class CarrierConnectionDetail(api.APIView):
98
+ @openapi.extend_schema(
99
+ tags=["Connections"],
100
+ operation_id=f"{ENDPOINT_ID}retrieve",
101
+ extensions={"x-operationId": "retrieveCarrierConnection"},
102
+ summary="Retrieve a connection",
103
+ responses={
104
+ 200: serializers.CarrierConnection(),
105
+ 400: serializers.ErrorResponse(),
106
+ 500: serializers.ErrorResponse(),
107
+ },
108
+ )
109
+ def get(self, request: request.Request, pk: str):
110
+ """Retrieve carrier connection."""
111
+ connection = models.Carrier.access_by(request).get(pk=pk)
112
+ return response.Response(serializers.CarrierConnection(connection).data)
113
+
114
+ @openapi.extend_schema(
115
+ tags=["Connections"],
116
+ operation_id=f"{ENDPOINT_ID}update",
117
+ extensions={"x-operationId": "updateCarrierConnection"},
118
+ summary="Update a connection",
119
+ request=serializers.CarrierConnectionData(),
120
+ responses={
121
+ 200: serializers.CarrierConnection(),
122
+ 400: serializers.ErrorResponse(),
123
+ 404: serializers.ErrorResponse(),
124
+ 500: serializers.ErrorResponse(),
125
+ },
126
+ )
127
+ def patch(self, request: request.Request, pk: str):
128
+ """Update a carrier connection."""
129
+ connection = models.Carrier.access_by(request).get(pk=pk)
130
+ update = lib.identity(
131
+ serializers.CarrierConnectionModelSerializer.map(
132
+ connection,
133
+ data=request.data,
134
+ context=request,
135
+ )
136
+ .save()
137
+ .instance
138
+ )
139
+
140
+ return response.Response(serializers.CarrierConnection(update).data)
141
+
142
+ @openapi.extend_schema(
143
+ tags=["Connections"],
144
+ operation_id=f"{ENDPOINT_ID}remove",
145
+ extensions={"x-operationId": "removeCarrierConnection"},
146
+ summary="Remove a carrier connection",
147
+ responses={
148
+ 200: serializers.CarrierConnection(),
149
+ 404: serializers.ErrorResponse(),
150
+ 409: serializers.ErrorResponse(),
151
+ 500: serializers.ErrorResponse(),
152
+ },
153
+ )
154
+ def delete(self, request: request.Request, pk: str):
155
+ """Remove a carrier connection."""
156
+ connection = models.Carrier.access_by(request).get(pk=pk)
157
+
158
+ connection.delete(keep_parents=True)
159
+
160
+ return response.Response(serializers.CarrierConnection(connection).data)
161
+
162
+
163
+ class ConnectionWebhookRegister(api.APIView):
164
+ @openapi.extend_schema(
165
+ exclude=True,
166
+ tags=["Connections"],
167
+ operation_id=f"{ENDPOINT_ID}webhook-register",
168
+ extensions={"x-operationId": "webhookRegister"},
169
+ summary="Register a webhook for a carrier connection",
170
+ request=serializers.WebhookRegisterData(),
171
+ responses={
172
+ 200: serializers.WebhookOperationResponse(),
173
+ 400: serializers.ErrorResponse(),
174
+ 424: serializers.ErrorMessages(),
175
+ },
176
+ )
177
+ def post(self, request: request.Request, pk: str):
178
+ """Register a webhook endpoint for a carrier connection."""
179
+ connection = models.Carrier.access_by(request).get(pk=pk)
180
+ webhook_url = request.build_absolute_uri(f"/v1/connections/webhook/{pk}/events")
181
+
182
+ webhook_details = (
183
+ serializers.WebhookRegisterSerializer.map(
184
+ connection,
185
+ data={"webhook_url": webhook_url, **request.data},
186
+ context=request,
187
+ )
188
+ .save()
189
+ .instance
190
+ )
191
+
192
+ updated = lib.identity(
193
+ serializers.CarrierConnectionModelSerializer.map(
194
+ connection,
195
+ data=dict(
196
+ config=dict(
197
+ webhook_id=webhook_details.webhook_identifier,
198
+ webhook_secret=webhook_details.secret,
199
+ webhook_url=webhook_url,
200
+ )
201
+ ),
202
+ context=request,
203
+ )
204
+ .save()
205
+ .instance
206
+ )
207
+
208
+ return response.Response(
209
+ serializers.WebhookOperationResponse(
210
+ dict(
211
+ carrier_name=updated.carrier_name,
212
+ carrier_id=updated.carrier_id,
213
+ operation="Webhook registration",
214
+ success=True,
215
+ )
216
+ ).data,
217
+ status=status.HTTP_200_OK,
218
+ )
219
+
220
+
221
+ class ConnectionWebhookDeregister(api.APIView):
222
+ @openapi.extend_schema(
223
+ exclude=True,
224
+ tags=["Connections"],
225
+ operation_id=f"{ENDPOINT_ID}webhook-deregister",
226
+ extensions={"x-operationId": "webhookDeregister"},
227
+ summary="Deregister a webhook for a carrier connection",
228
+ responses={
229
+ 200: serializers.WebhookOperationResponse(),
230
+ 400: serializers.ErrorResponse(),
231
+ 424: serializers.ErrorMessages(),
232
+ },
233
+ )
234
+ def post(self, request: request.Request, pk: str):
235
+ """Deregister a webhook endpoint from a carrier connection."""
236
+ connection = models.Carrier.access_by(request).get(pk=pk)
237
+
238
+ serializers.WebhookDeregisterSerializer.map(
239
+ connection,
240
+ data=dict(webhook_id=connection.config.get("webhook_id")),
241
+ context=request,
242
+ ).save()
243
+
244
+ updated = lib.identity(
245
+ serializers.CarrierConnectionModelSerializer.map(
246
+ connection,
247
+ data=dict(
248
+ config=dict(
249
+ webhook_id=None,
250
+ webhook_secret=None,
251
+ webhook_url=None,
252
+ )
253
+ ),
254
+ context=request,
255
+ )
256
+ .save()
257
+ .instance
258
+ )
259
+
260
+ return response.Response(
261
+ serializers.WebhookOperationResponse(
262
+ dict(
263
+ operation="Webhook deregistration",
264
+ success=True,
265
+ carrier_name=updated.carrier_name,
266
+ carrier_id=updated.carrier_id,
267
+ )
268
+ ).data,
269
+ status=status.HTTP_200_OK,
270
+ )
271
+
272
+
273
+ class ConnectionWebhookDisconnect(api.APIView):
274
+ @openapi.extend_schema(
275
+ exclude=True,
276
+ tags=["Connections"],
277
+ operation_id=f"{ENDPOINT_ID}webhook-disconnect",
278
+ extensions={"x-operationId": "webhookDisconnect"},
279
+ summary="Force disconnect a webhook for a carrier connection",
280
+ responses={
281
+ 200: serializers.WebhookOperationResponse(),
282
+ 400: serializers.ErrorResponse(),
283
+ },
284
+ )
285
+ def post(self, request: request.Request, pk: str):
286
+ """Force disconnect a webhook from a carrier connection (local only)."""
287
+ connection = models.Carrier.access_by(request).get(pk=pk)
288
+
289
+ updated = lib.identity(
290
+ serializers.CarrierConnectionModelSerializer.map(
291
+ connection,
292
+ data=dict(
293
+ config=dict(
294
+ webhook_id=None,
295
+ webhook_secret=None,
296
+ webhook_url=None,
297
+ )
298
+ ),
299
+ context=request,
300
+ )
301
+ .save()
302
+ .instance
303
+ )
304
+
305
+ return response.Response(
306
+ serializers.WebhookOperationResponse(
307
+ dict(
308
+ operation="Webhook disconnect",
309
+ success=True,
310
+ carrier_name=updated.carrier_name,
311
+ carrier_id=updated.carrier_id,
312
+ )
313
+ ).data,
314
+ status=status.HTTP_200_OK,
315
+ )
316
+
317
+
318
+ class ConnectionWebhookEvent(api.APIView):
319
+ """Handle inbound webhook events from carriers."""
320
+
321
+ throttle_classes: list = []
322
+ permission_classes: list = []
323
+ authentication_classes: list = []
324
+
325
+ @openapi.extend_schema(
326
+ exclude=True,
327
+ tags=["Connections"],
328
+ operation_id=f"{ENDPOINT_ID}webhook-event",
329
+ extensions={"x-operationId": "webhookEvent"},
330
+ summary="Handle carrier webhook events",
331
+ responses={
332
+ 200: serializers.OperationConfirmation(),
333
+ },
334
+ )
335
+ def post(self, request: request.Request, pk: str):
336
+ """Handle inbound webhook events from a carrier via POST."""
337
+ data, http_status = serializers.WebhookEventSerializer.process_event(
338
+ request, pk
339
+ )
340
+ return response.Response(data, status=http_status)
341
+
342
+
343
+ class ConnectionOauthAuthorize(api.APIView):
344
+ @openapi.extend_schema(
345
+ exclude=True,
346
+ tags=["Connections"],
347
+ operation_id=f"{ENDPOINT_ID}oauth-authorize",
348
+ extensions={"x-operationId": "oauthAuthorize"},
349
+ summary="Handle an OAuth authorize",
350
+ request=serializers.OAuthAuthorizeData(),
351
+ )
352
+ def post(self, request: request.Request, carrier_name: str):
353
+ """Handle an OAuth authorize."""
354
+
355
+ [output, messages] = gateway.Hooks.on_oauth_authorize(
356
+ serializers.OAuthAuthorizeData.map(
357
+ data={
358
+ "redirect_uri": request.build_absolute_uri(
359
+ f"/v1/connections/oauth/{carrier_name}/callback"
360
+ ),
361
+ **request.data,
362
+ }
363
+ ).data,
364
+ carrier_name=carrier_name,
365
+ test_mode=request.test_mode,
366
+ )
367
+
368
+ # Include the frontend_url for the callback to redirect to
369
+ frontend_url = request.data.get("frontend_url")
370
+
371
+ return response.Response(
372
+ dict(
373
+ operation="OAuth authorize",
374
+ request=lib.to_dict(output),
375
+ messages=lib.to_dict(messages),
376
+ frontend_url=frontend_url,
377
+ ),
378
+ status=status.HTTP_200_OK,
379
+ )
380
+
381
+
382
+ class ConnectionOauthCallback(api.APIView):
383
+ @openapi.extend_schema(
384
+ exclude=True,
385
+ tags=["Connections"],
386
+ operation_id=f"{ENDPOINT_ID}oauth-callback",
387
+ extensions={"x-operationId": "oauthCallback"},
388
+ summary="Handle an OAuth callback",
389
+ request=serializers.OAuthCallbackData(),
390
+ responses={
391
+ 200: serializers.OperationConfirmation(),
392
+ },
393
+ )
394
+ def get(self, request: request.Request, carrier_name: str):
395
+ """Handle an OAuth callback via GET."""
396
+ return self._handle_callback(request, carrier_name)
397
+
398
+ @openapi.extend_schema(
399
+ exclude=True,
400
+ tags=["Connections"],
401
+ operation_id=f"{ENDPOINT_ID}oauth-callback-post",
402
+ extensions={"x-operationId": "oauthCallbackPost"},
403
+ summary="Handle an OAuth callback via POST",
404
+ request=serializers.OAuthCallbackData(),
405
+ responses={
406
+ 200: serializers.OperationConfirmation(),
407
+ },
408
+ )
409
+ def post(self, request: request.Request, carrier_name: str):
410
+ """Handle an OAuth callback via POST."""
411
+ return self._handle_callback(request, carrier_name)
412
+
413
+ def _handle_callback(self, request: request.Request, carrier_name: str):
414
+ """Handle OAuth callback processing.
415
+
416
+ Returns JSON with OAuth result for the frontend to process.
417
+ When called from a browser popup, renders template or redirects to frontend.
418
+ """
419
+ import json
420
+ import base64
421
+ from django.shortcuts import render
422
+ from django.http import HttpResponseRedirect
423
+
424
+ result, frontend_url = serializers.OAuthCallbackSerializer.process_callback(
425
+ request, carrier_name
426
+ )
427
+
428
+ accept_header = request.headers.get("Accept", "")
429
+
430
+ if "text/html" in accept_header and frontend_url:
431
+ result_encoded = base64.b64encode(
432
+ json.dumps(result).encode("utf-8")
433
+ ).decode("utf-8")
434
+ return HttpResponseRedirect(f"{frontend_url}?oauth_result={result_encoded}")
435
+
436
+ if "text/html" in accept_header:
437
+ error_message = (
438
+ result["messages"][0]["message"]
439
+ if result["messages"]
440
+ else "An error occurred during authorization."
441
+ )
442
+ return render(
443
+ request._request,
444
+ "providers/oauth_callback.html",
445
+ dict(
446
+ success=result["success"],
447
+ error_message=error_message,
448
+ result_json=json.dumps(result),
449
+ ),
450
+ )
451
+
452
+ return response.Response(result, status=status.HTTP_200_OK)
453
+
454
+
455
+ urlpatterns = [
456
+ urls.path(
457
+ "connections",
458
+ CarrierConnectionListView.as_view(),
459
+ name="carrier-connection-list",
460
+ ),
461
+ urls.path(
462
+ "connections/<str:pk>",
463
+ CarrierConnectionDetail.as_view(),
464
+ name="carrier-connection-details",
465
+ ),
466
+ urls.path(
467
+ "connections/oauth/<str:carrier_name>/authorize",
468
+ ConnectionOauthAuthorize.as_view(),
469
+ name="connection-oauth-authorize",
470
+ ),
471
+ urls.path(
472
+ "connections/oauth/<str:carrier_name>/callback",
473
+ ConnectionOauthCallback.as_view(),
474
+ name="connection-oauth-callback",
475
+ ),
476
+ urls.path(
477
+ "connections/webhook/<str:pk>/events",
478
+ ConnectionWebhookEvent.as_view(),
479
+ name="connection-webhook-event",
480
+ ),
481
+ urls.path(
482
+ "connections/webhook/<str:pk>/register",
483
+ ConnectionWebhookRegister.as_view(),
484
+ name="connection-webhook-register",
485
+ ),
486
+ urls.path(
487
+ "connections/webhook/<str:pk>/deregister",
488
+ ConnectionWebhookDeregister.as_view(),
489
+ name="connection-webhook-deregister",
490
+ ),
491
+ urls.path(
492
+ "connections/webhook/<str:pk>/disconnect",
493
+ ConnectionWebhookDisconnect.as_view(),
494
+ name="connection-webhook-disconnect",
495
+ ),
496
+ ]