karrio-server-graph 2025.5rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. karrio/server/graph/__init__.py +1 -0
  2. karrio/server/graph/admin.py +3 -0
  3. karrio/server/graph/apps.py +5 -0
  4. karrio/server/graph/forms.py +59 -0
  5. karrio/server/graph/management/__init__.py +0 -0
  6. karrio/server/graph/management/commands/__init__.py +0 -0
  7. karrio/server/graph/management/commands/export_schema.py +9 -0
  8. karrio/server/graph/migrations/0001_initial.py +37 -0
  9. karrio/server/graph/migrations/0002_auto_20210512_1353.py +22 -0
  10. karrio/server/graph/migrations/__init__.py +0 -0
  11. karrio/server/graph/models.py +44 -0
  12. karrio/server/graph/schema.py +46 -0
  13. karrio/server/graph/schemas/__init__.py +2 -0
  14. karrio/server/graph/schemas/base/__init__.py +367 -0
  15. karrio/server/graph/schemas/base/inputs.py +582 -0
  16. karrio/server/graph/schemas/base/mutations.py +871 -0
  17. karrio/server/graph/schemas/base/types.py +1365 -0
  18. karrio/server/graph/serializers.py +388 -0
  19. karrio/server/graph/templates/graphql/graphiql.html +142 -0
  20. karrio/server/graph/templates/karrio/email_change_email.html +13 -0
  21. karrio/server/graph/templates/karrio/email_change_email.txt +13 -0
  22. karrio/server/graph/templates/karrio/password_reset_email.html +14 -0
  23. karrio/server/graph/tests/__init__.py +9 -0
  24. karrio/server/graph/tests/base.py +124 -0
  25. karrio/server/graph/tests/test_carrier_connections.py +219 -0
  26. karrio/server/graph/tests/test_metafield.py +404 -0
  27. karrio/server/graph/tests/test_rate_sheets.py +348 -0
  28. karrio/server/graph/tests/test_templates.py +677 -0
  29. karrio/server/graph/tests/test_user_info.py +71 -0
  30. karrio/server/graph/urls.py +10 -0
  31. karrio/server/graph/utils.py +304 -0
  32. karrio/server/graph/views.py +93 -0
  33. karrio/server/settings/graph.py +7 -0
  34. karrio_server_graph-2025.5rc1.dist-info/METADATA +29 -0
  35. karrio_server_graph-2025.5rc1.dist-info/RECORD +37 -0
  36. karrio_server_graph-2025.5rc1.dist-info/WHEEL +5 -0
  37. karrio_server_graph-2025.5rc1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,871 @@
1
+ import strawberry
2
+ import typing
3
+ import logging
4
+ import datetime
5
+ from strawberry.types import Info
6
+ from rest_framework import exceptions
7
+ from django.utils.http import urlsafe_base64_decode
8
+ from django.contrib.contenttypes.models import ContentType
9
+ from django_email_verification import confirm as email_verification
10
+ from django_otp.plugins.otp_email import models as otp
11
+ from django.utils.translation import gettext_lazy as _
12
+ from django.db import transaction
13
+
14
+ from karrio.server.core.utils import ConfirmationToken, send_email
15
+ from karrio.server.user.serializers import TokenSerializer
16
+ from karrio.server.conf import settings
17
+ from karrio.server.serializers import (
18
+ save_many_to_many_data,
19
+ process_dictionaries_mutations,
20
+ )
21
+ import karrio.server.providers.serializers as providers_serializers
22
+ import karrio.server.manager.serializers as manager_serializers
23
+ import karrio.server.graph.schemas.base.inputs as inputs
24
+ import karrio.server.graph.schemas.base.types as types
25
+ import karrio.server.graph.serializers as serializers
26
+ import karrio.server.providers.models as providers
27
+ import karrio.server.manager.models as manager
28
+ import karrio.server.user.forms as user_forms
29
+ import karrio.server.core.gateway as gateway
30
+ import karrio.server.graph.models as graph
31
+ import karrio.server.graph.forms as forms
32
+ import karrio.server.graph.utils as utils
33
+ import karrio.server.user.models as auth
34
+ import karrio.server.iam.models as iam
35
+ import karrio.server.core.models as core
36
+ import karrio.lib as lib
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ @strawberry.type
42
+ class UserUpdateMutation(utils.BaseMutation):
43
+ user: typing.Optional[types.UserType] = None
44
+
45
+ @staticmethod
46
+ @utils.authentication_required
47
+ @utils.authorization_required()
48
+ def mutate(info: Info, **input: inputs.UpdateUserInput) -> "UserUpdateMutation":
49
+ instance = types.User.objects.get(id=info.context.request.user.id)
50
+
51
+ serializer = serializers.UserModelSerializer(
52
+ instance,
53
+ partial=True,
54
+ data=input,
55
+ context=info.context.request,
56
+ )
57
+
58
+ serializer.is_valid(raise_exception=True)
59
+
60
+ return UserUpdateMutation(user=serializer.save()) # type:ignore
61
+
62
+
63
+ @strawberry.type
64
+ class WorkspaceConfigMutation(utils.BaseMutation):
65
+ workspace_config: typing.Optional[types.WorkspaceConfigType] = None
66
+
67
+ @staticmethod
68
+ @utils.authentication_required
69
+ @utils.authorization_required(["manage_team"])
70
+ def mutate(
71
+ info: Info, **input: inputs.WorkspaceConfigMutationInput
72
+ ) -> "WorkspaceConfigMutation":
73
+ data = dict(config=input.copy())
74
+ workspace = auth.WorkspaceConfig.access_by(info.context.request).first()
75
+
76
+ serializer = serializers.WorkspaceConfigModelSerializer(
77
+ workspace,
78
+ partial=workspace is not None,
79
+ data=process_dictionaries_mutations(["config"], data, workspace),
80
+ context=info.context.request,
81
+ )
82
+
83
+ serializer.is_valid(raise_exception=True)
84
+
85
+ return WorkspaceConfigMutation(
86
+ workspace_config=serializer.save()
87
+ ) # type:ignore
88
+
89
+
90
+ @strawberry.type
91
+ class TokenMutation(utils.BaseMutation):
92
+ token: typing.Optional[types.TokenType] = None
93
+
94
+ @staticmethod
95
+ @utils.authentication_required
96
+ @utils.authorization_required()
97
+ def mutate(
98
+ info: Info,
99
+ key: str = None,
100
+ refresh: bool = None,
101
+ password: str = None,
102
+ ) -> "UserUpdateMutation":
103
+ tokens = auth.Token.access_by(info.context.request).filter(key=key)
104
+
105
+ if refresh:
106
+ if len(password or "") == 0:
107
+ raise exceptions.ValidationError(
108
+ {"password": "Password is required to refresh token"}
109
+ )
110
+
111
+ if not info.context.request.user.check_password(password):
112
+ raise exceptions.ValidationError({"password": "Invalid password"})
113
+
114
+ if any(tokens):
115
+ tokens.delete()
116
+
117
+ else:
118
+ return TokenMutation(token=tokens.first()) # type:ignore
119
+
120
+ token = (
121
+ TokenSerializer.map(data={}, context=info.context.request).save().instance
122
+ )
123
+
124
+ return TokenMutation(token=token) # type:ignore
125
+
126
+
127
+ @strawberry.type
128
+ class CreateAPIKeyMutation(utils.BaseMutation):
129
+ api_key: typing.Optional[types.APIKeyType] = None
130
+
131
+ @staticmethod
132
+ @transaction.atomic
133
+ @utils.authentication_required
134
+ @utils.authorization_required()
135
+ @utils.password_required
136
+ def mutate(
137
+ info: Info, password: str, **input: inputs.CreateAPIKeyMutationInput
138
+ ) -> "CreateAPIKeyMutation":
139
+ context = info.context.request
140
+ data = input.copy()
141
+ permissions = data.pop("permissions", [])
142
+ api_key = TokenSerializer.map(data=data, context=context).save().instance
143
+
144
+ if any(permissions):
145
+ _auth_ctx = (
146
+ context.token
147
+ if hasattr(getattr(info.context.request, "token", None), "permissions")
148
+ else context.user
149
+ )
150
+ _ctx_permissions = getattr(_auth_ctx, "permissions", [])
151
+ _invalid_permissions = [_ for _ in permissions if _ not in _ctx_permissions]
152
+
153
+ if any(_invalid_permissions):
154
+ raise exceptions.ValidationError({"permissions": "Invalid permissions"})
155
+
156
+ _ctx = iam.ContextPermission.objects.create(
157
+ object_pk=api_key.pk,
158
+ content_object=api_key,
159
+ content_type=ContentType.objects.get_for_model(api_key),
160
+ )
161
+ _ctx.groups.set(auth.Group.objects.filter(name__in=permissions))
162
+
163
+ return CreateAPIKeyMutation(
164
+ api_key=auth.Token.access_by(context).get(key=api_key.key)
165
+ ) # type:ignore
166
+
167
+
168
+ @strawberry.type
169
+ class DeleteAPIKeyMutation(utils.BaseMutation):
170
+ label: typing.Optional[str] = None
171
+
172
+ @staticmethod
173
+ @utils.authentication_required
174
+ @utils.authorization_required()
175
+ @utils.password_required
176
+ def mutate(
177
+ info: Info, password: str, **input: inputs.DeleteAPIKeyMutationInput
178
+ ) -> "DeleteAPIKeyMutation":
179
+ api_key = auth.Token.access_by(info.context.request).get(**input)
180
+ label = api_key.label
181
+ api_key.delete()
182
+
183
+ return DeleteAPIKeyMutation(label=label) # type:ignore
184
+
185
+
186
+ @strawberry.type
187
+ class RequestEmailChangeMutation(utils.BaseMutation):
188
+ user: typing.Optional[types.UserType] = None
189
+
190
+ @staticmethod
191
+ @utils.authentication_required
192
+ @utils.authorization_required()
193
+ @utils.password_required
194
+ def mutate(
195
+ info: Info, email: str, password: str, redirect_url: str
196
+ ) -> "RequestEmailChangeMutation":
197
+ try:
198
+ token = ConfirmationToken.for_data(
199
+ user=info.context.request.user,
200
+ data=dict(new_email=email),
201
+ )
202
+
203
+ send_email(
204
+ emails=[email],
205
+ subject="Confirm your new email address",
206
+ email_template="karrio/email_change_email.html",
207
+ text_template="karrio/email_change_email.txt",
208
+ context=dict(
209
+ token=token,
210
+ link=redirect_url,
211
+ ),
212
+ expiry=(datetime.datetime.now() + datetime.timedelta(hours=2)),
213
+ )
214
+ except Exception as e:
215
+ logger.exception(e)
216
+ raise e
217
+
218
+ return RequestEmailChangeMutation(user=info.context.request.user) # type:ignore
219
+
220
+
221
+ @strawberry.type
222
+ class ConfirmEmailChangeMutation(utils.BaseMutation):
223
+ user: typing.Optional[types.UserType] = None
224
+
225
+ @staticmethod
226
+ @utils.authentication_required
227
+ @utils.authorization_required()
228
+ def mutate(info: Info, token: str) -> "ConfirmEmailChangeMutation":
229
+ validated_token = ConfirmationToken(token)
230
+ user = info.context.request.user
231
+
232
+ if user.id != validated_token["user_id"]:
233
+ raise exceptions.ValidationError(
234
+ {"token": "auth.Token is invalid or expired"}
235
+ )
236
+
237
+ if user.email == validated_token["new_email"]:
238
+ raise exceptions.APIException("Email is already confirmed")
239
+
240
+ user.email = validated_token["new_email"]
241
+ user.save()
242
+
243
+ return ConfirmEmailChangeMutation(
244
+ user=types.User.objects.get(id=validated_token["user_id"])
245
+ ) # type:ignore
246
+
247
+
248
+ @strawberry.type
249
+ class RegisterUserMutation(utils.BaseMutation):
250
+ user: typing.Optional[types.UserType] = None
251
+
252
+ @staticmethod
253
+ def mutate(
254
+ info: Info, **input: inputs.RegisterUserMutationInput
255
+ ) -> "RegisterUserMutation":
256
+ if settings.ALLOW_SIGNUP == False:
257
+ raise Exception(
258
+ "Signup is not allowed. "
259
+ "Please contact your administrator to create an account."
260
+ )
261
+
262
+ try:
263
+ form = user_forms.SignUpForm(input)
264
+ user = form.save()
265
+
266
+ return RegisterUserMutation(user=user) # type:ignore
267
+ except Exception as e:
268
+ logger.exception(e)
269
+ raise e
270
+
271
+
272
+ @strawberry.type
273
+ class ConfirmEmailMutation(utils.BaseMutation):
274
+ success: bool = False
275
+
276
+ @staticmethod
277
+ def mutate(info: Info, token: str) -> "ConfirmEmailMutation":
278
+ try:
279
+ success, _ = email_verification.verify_token(token)
280
+
281
+ return ConfirmEmailMutation(success=success) # type:ignore
282
+ except Exception as e:
283
+ logger.exception(e)
284
+ raise e
285
+
286
+
287
+ @strawberry.type
288
+ class ChangePasswordMutation(utils.BaseMutation):
289
+ user: typing.Optional[types.UserType] = None
290
+
291
+ @staticmethod
292
+ @utils.authentication_required
293
+ @utils.authorization_required()
294
+ def mutate(
295
+ info: Info, **input: inputs.ChangePasswordMutationInput
296
+ ) -> "ChangePasswordMutation":
297
+ form = forms.PasswordChangeForm(info.context.request.user, data=input)
298
+ if form.is_valid():
299
+ user = form.save()
300
+ return ChangePasswordMutation(user=user) # type:ignore
301
+ else:
302
+ raise exceptions.ValidationError(form.errors)
303
+
304
+
305
+ @strawberry.type
306
+ class RequestPasswordResetMutation(utils.BaseMutation):
307
+ email: str = strawberry.UNSET
308
+ redirect_url: str = strawberry.UNSET
309
+
310
+ @staticmethod
311
+ def mutate(
312
+ info: Info, **input: inputs.RequestPasswordResetMutationInput
313
+ ) -> "RequestPasswordResetMutation":
314
+ form = forms.ResetPasswordRequestForm(data=input)
315
+
316
+ if form.is_valid():
317
+ form.save(request=info.context.request)
318
+ return RequestPasswordResetMutation(**form.cleaned_data) # type:ignore
319
+ else:
320
+ raise exceptions.ValidationError(form.errors)
321
+
322
+
323
+ @strawberry.type
324
+ class ConfirmPasswordResetMutation(utils.BaseMutation):
325
+ user: typing.Optional[types.UserType] = None
326
+
327
+ @staticmethod
328
+ def get_user(uidb64):
329
+ try:
330
+ uid = urlsafe_base64_decode(uidb64).decode()
331
+ user = types.User._default_manager.get(pk=uid)
332
+ except (
333
+ TypeError,
334
+ ValueError,
335
+ OverflowError,
336
+ types.User.DoesNotExist,
337
+ exceptions.ValidationError,
338
+ ):
339
+ user = None
340
+ return user
341
+
342
+ @staticmethod
343
+ def mutate(
344
+ info: Info, **input: inputs.ConfirmPasswordResetMutationInput
345
+ ) -> "ConfirmPasswordResetMutation":
346
+ uuid = input.get("uid")
347
+ user = ConfirmPasswordResetMutation.get_user(uuid) # type:ignore
348
+ form = forms.ConfirmPasswordResetForm(user, data=input)
349
+
350
+ if form.is_valid():
351
+ return ConfirmPasswordResetMutation(user=form.save()) # type:ignore
352
+ else:
353
+ raise exceptions.ValidationError(form.errors)
354
+
355
+
356
+ @strawberry.type
357
+ class EnableMultiFactorMutation(utils.BaseMutation):
358
+ user: typing.Optional[types.UserType] = None
359
+
360
+ @staticmethod
361
+ @utils.authentication_required
362
+ @utils.authorization_required()
363
+ def mutate(
364
+ info: Info, **input: inputs.EnableMultiFactorMutationInput
365
+ ) -> "EnableMultiFactorMutation":
366
+ # Retrieve a default device or create a new one.
367
+ device = otp.EmailDevice.objects.filter(
368
+ user__id=info.context.request.user.id
369
+ ).first()
370
+ if device is None:
371
+ device = otp.EmailDevice.objects.create(
372
+ name="default",
373
+ confirmed=False,
374
+ user=info.context.request.user,
375
+ )
376
+
377
+ # Send and email challenge if the device isn't confirmed yet.
378
+ if device.confirmed == False:
379
+ device.generate_challenge()
380
+
381
+ return EnableMultiFactorMutation( # type:ignore
382
+ user=types.User.objects.get(pk=info.context.request.user.id)
383
+ )
384
+
385
+
386
+ @strawberry.type
387
+ class ConfirmMultiFactorMutation(utils.BaseMutation):
388
+ user: typing.Optional[types.UserType] = None
389
+
390
+ @staticmethod
391
+ @utils.authentication_required
392
+ @utils.authorization_required()
393
+ def mutate(
394
+ info: Info, **input: inputs.ConfirmMultiFactorMutationInput
395
+ ) -> "ConfirmMultiFactorMutation":
396
+ token = input.get("token")
397
+ # Retrieve a default device or create a new one.
398
+ device = otp.EmailDevice.objects.filter(
399
+ user__id=info.context.request.user.id
400
+ ).first()
401
+
402
+ if device is None:
403
+ raise exceptions.APIException(
404
+ _("You need to enable Two factor auth first."), code="2fa_disabled"
405
+ )
406
+
407
+ # check if token is valid
408
+ if device.verify_token(token) is False:
409
+ raise exceptions.ValidationError(
410
+ {"otp_token": _("Invalid or Expired OTP token")}, code="otp_invalid"
411
+ )
412
+
413
+ device.confirmed = True
414
+ device.save()
415
+
416
+ return ConfirmMultiFactorMutation( # type:ignore
417
+ user=types.User.objects.get(pk=info.context.request.user.id)
418
+ )
419
+
420
+
421
+ @strawberry.type
422
+ class DisableMultiFactorMutation(utils.BaseMutation):
423
+ user: typing.Optional[types.UserType] = None
424
+
425
+ @staticmethod
426
+ @utils.authentication_required
427
+ @utils.authorization_required()
428
+ def mutate(
429
+ info: Info, **input: inputs.DisableMultiFactorMutationInput
430
+ ) -> "DisableMultiFactorMutation":
431
+ # Retrieve a default device or create a new one.
432
+ device = otp.EmailDevice.objects.filter(
433
+ user__id=info.context.request.user.id
434
+ ).first()
435
+ if device is not None:
436
+ device.delete()
437
+
438
+ return DisableMultiFactorMutation( # type:ignore
439
+ user=types.User.objects.get(pk=info.context.request.user.id)
440
+ )
441
+
442
+
443
+ @strawberry.type
444
+ class MetadataMutation(utils.BaseMutation):
445
+ id: str = strawberry.UNSET
446
+ metadata: utils.JSON = strawberry.UNSET
447
+
448
+ @staticmethod
449
+ @utils.authentication_required
450
+ @utils.authorization_required()
451
+ def mutate(
452
+ info: Info,
453
+ id: str,
454
+ object_type: utils.MetadataObjectTypeEnum,
455
+ added_values: dict = {},
456
+ discarded_keys: list = [],
457
+ ) -> "MetadataMutation":
458
+ object_model = utils.MetadataObjectType[object_type].value
459
+ instance = object_model.access_by(info.context.request).get(id=id)
460
+ instance.metadata = {
461
+ key: value
462
+ for key, value in (instance.metadata or {}).items()
463
+ if key not in discarded_keys
464
+ }
465
+ instance.metadata.update(added_values)
466
+ instance.save(update_fields=["metadata"])
467
+
468
+ return MetadataMutation(id=id, metadata=instance.metadata) # type:ignore
469
+
470
+
471
+ @strawberry.type
472
+ class DeleteMutation(utils.BaseMutation):
473
+ id: str = strawberry.UNSET
474
+
475
+ @staticmethod
476
+ @utils.authentication_required
477
+ @utils.authorization_required()
478
+ def mutate(
479
+ info: Info,
480
+ model,
481
+ validator: typing.Callable = None,
482
+ **input: inputs.DeleteMutationInput,
483
+ ) -> "DeleteMutation":
484
+ id = input.get("id")
485
+ queryset = (
486
+ model.access_by(info.context.request)
487
+ if hasattr(model, "access_by")
488
+ else model.objects
489
+ )
490
+ instance = queryset.get(id=id)
491
+
492
+ if validator:
493
+ validator(instance, context=info.context)
494
+
495
+ shipment = getattr(instance, "shipment", None)
496
+ instance.delete(keep_parents=True)
497
+
498
+ if shipment is not None:
499
+ manager_serializers.reset_related_shipment_rates(shipment)
500
+
501
+ return DeleteMutation(id=id) # type:ignore
502
+
503
+
504
+ @strawberry.type
505
+ class CreateRateSheetMutation(utils.BaseMutation):
506
+ rate_sheet: typing.Optional[types.RateSheetType] = None
507
+
508
+ @staticmethod
509
+ @transaction.atomic
510
+ @utils.authentication_required
511
+ @utils.authorization_required(["manage_carriers"])
512
+ def mutate(
513
+ info: Info, **input: inputs.CreateRateSheetMutationInput
514
+ ) -> "CreateRateSheetMutation":
515
+ data = input.copy()
516
+ carriers = data.pop("carriers", [])
517
+ slug = f"{input.get('name', '').lower()}_sheet".replace(" ", "").lower()
518
+ serializer = serializers.RateSheetModelSerializer(
519
+ data={**data, "slug": slug},
520
+ context=info.context.request,
521
+ )
522
+
523
+ serializer.is_valid(raise_exception=True)
524
+ rate_sheet = serializer.save()
525
+
526
+ if "services" in data:
527
+ save_many_to_many_data(
528
+ "services",
529
+ serializers.ServiceLevelModelSerializer,
530
+ rate_sheet,
531
+ payload=data,
532
+ context=info.context.request,
533
+ )
534
+
535
+ if any(carriers):
536
+ _carriers = gateway.Carriers.list(
537
+ context=info.context.request,
538
+ carrier_name=rate_sheet.carrier_name,
539
+ ).filter(id__in=carriers)
540
+ for _ in _carriers:
541
+ _.settings.rate_sheet = rate_sheet
542
+ _.settings.save(update_fields=["rate_sheet"])
543
+
544
+ return CreateRateSheetMutation(rate_sheet=rate_sheet)
545
+
546
+
547
+ @strawberry.type
548
+ class UpdateRateSheetMutation(utils.BaseMutation):
549
+ rate_sheet: typing.Optional[types.RateSheetType] = None
550
+
551
+ @staticmethod
552
+ @transaction.atomic
553
+ @utils.authentication_required
554
+ @utils.authorization_required(["manage_carriers"])
555
+ def mutate(
556
+ info: Info, **input: inputs.UpdateRateSheetMutationInput
557
+ ) -> "UpdateRateSheetMutation":
558
+ instance = providers.RateSheet.access_by(info.context.request).get(
559
+ id=input["id"]
560
+ )
561
+ serializer = serializers.RateSheetModelSerializer(
562
+ instance,
563
+ data=input,
564
+ context=info.context.request,
565
+ partial=True,
566
+ )
567
+
568
+ serializer.is_valid(raise_exception=True)
569
+ rate_sheet = serializer.save()
570
+
571
+ return UpdateRateSheetMutation(
572
+ rate_sheet=providers.RateSheet.objects.get(id=input["id"])
573
+ )
574
+
575
+
576
+ @strawberry.type
577
+ class PartialShipmentMutation(utils.BaseMutation):
578
+ shipment: typing.Optional[types.ShipmentType] = None
579
+
580
+ @staticmethod
581
+ @utils.authentication_required
582
+ @utils.authorization_required(["manage_shipments"])
583
+ def mutate(
584
+ info: Info, **input: inputs.PartialShipmentMutationInput
585
+ ) -> "PartialShipmentMutation":
586
+ try:
587
+ id = input["id"]
588
+ shipment = manager.Shipment.access_by(info.context.request).get(id=id)
589
+ manager_serializers.can_mutate_shipment(
590
+ shipment, update=True, payload=input
591
+ )
592
+
593
+ manager_serializers.ShipmentSerializer.map(
594
+ shipment,
595
+ context=info.context.request,
596
+ data=process_dictionaries_mutations(["options"], input, shipment),
597
+ ).save()
598
+
599
+ # refetch the shipment to get the updated state with signals processed
600
+ update = manager.Shipment.access_by(info.context.request).get(id=id)
601
+
602
+ return PartialShipmentMutation(errors=None, shipment=update) # type:ignore
603
+ except Exception as e:
604
+ logger.exception(e)
605
+ raise e
606
+
607
+
608
+ @strawberry.type
609
+ class ChangeShipmentStatusMutation(utils.BaseMutation):
610
+ shipment: typing.Optional[types.ShipmentType] = None
611
+
612
+ @staticmethod
613
+ @utils.authentication_required
614
+ @utils.authorization_required(["manage_shipments"])
615
+ def mutate(
616
+ info: Info, **input: inputs.ChangeShipmentStatusMutationInput
617
+ ) -> "ChangeShipmentStatusMutation":
618
+ shipment = manager.Shipment.access_by(info.context.request).get(id=input["id"])
619
+
620
+ if shipment.status in [
621
+ utils.ShipmentStatusEnum.draft.name,
622
+ utils.ShipmentStatusEnum.cancelled.name,
623
+ ]:
624
+ raise exceptions.ValidationError(
625
+ _(f"{shipment.status} shipment cannot be changed to {input['status']}"),
626
+ code="invalid_operation",
627
+ )
628
+
629
+ if getattr(shipment, "tracker", None) is not None:
630
+ raise exceptions.ValidationError(
631
+ _(f"this shipment is tracked automatically by API"),
632
+ code="invalid_operation",
633
+ )
634
+
635
+ shipment.status = input["status"]
636
+ shipment.save(update_fields=["status"])
637
+
638
+ return ChangeShipmentStatusMutation(shipment=shipment) # type:ignore
639
+
640
+
641
+ def create_template_mutation(name: str, template_type: str) -> typing.Type:
642
+ _type: typing.Any = dict(
643
+ address=types.AddressTemplateType,
644
+ customs=types.CustomsTemplateType,
645
+ parcel=types.ParcelTemplateType,
646
+ ).get(template_type)
647
+
648
+ @strawberry.type
649
+ class _Mutation(utils.BaseMutation):
650
+ template: typing.Optional[_type] = None
651
+
652
+ @staticmethod
653
+ @utils.authentication_required
654
+ @utils.authorization_required()
655
+ @transaction.atomic
656
+ def mutate(info: Info, **input) -> name: # type:ignore
657
+ data = input.copy()
658
+ instance = (
659
+ graph.Template.access_by(info.context.request).get(id=input["id"])
660
+ if "id" in input
661
+ else None
662
+ )
663
+ customs_data = data.get("customs", {})
664
+
665
+ if "commodities" in customs_data and instance is not None:
666
+ save_many_to_many_data(
667
+ "commodities",
668
+ serializers.CommodityModelSerializer,
669
+ getattr(instance, "customs", None),
670
+ payload=customs_data,
671
+ context=info.context.request,
672
+ )
673
+
674
+ serializer = serializers.TemplateModelSerializer(
675
+ instance,
676
+ data=data,
677
+ context=info.context.request,
678
+ partial=(instance is not None),
679
+ )
680
+
681
+ serializer.is_valid(raise_exception=True)
682
+ template = serializer.save()
683
+
684
+ return _Mutation(template=template) # type:ignore
685
+
686
+ return strawberry.type(type(name, (_Mutation,), {}))
687
+
688
+
689
+ CreateAddressTemplateMutation = create_template_mutation(
690
+ "CreateAddressTemplateMutation", "address"
691
+ )
692
+ UpdateAddressTemplateMutation = create_template_mutation(
693
+ "UpdateAddressTemplateMutation",
694
+ "address",
695
+ )
696
+ CreateCustomsTemplateMutation = create_template_mutation(
697
+ "CreateCustomsTemplateMutation", "customs"
698
+ )
699
+ UpdateCustomsTemplateMutation = create_template_mutation(
700
+ "UpdateCustomsTemplateMutation", "customs"
701
+ )
702
+ CreateParcelTemplateMutation = create_template_mutation(
703
+ "CreateParcelTemplateMutation", "parcel"
704
+ )
705
+ UpdateParcelTemplateMutation = create_template_mutation(
706
+ "UpdateParcelTemplateMutation", "parcel"
707
+ )
708
+
709
+
710
+ @strawberry.type
711
+ class CreateCarrierConnectionMutation(utils.BaseMutation):
712
+ connection: types.CarrierConnectionType = None
713
+
714
+ @staticmethod
715
+ @utils.error_wrapper
716
+ @utils.authentication_required
717
+ @utils.authorization_required(["manage_carriers"])
718
+ def mutate(info: Info, **input) -> "CreateCarrierConnectionMutation":
719
+ data = input.copy()
720
+
721
+ connection = lib.identity(
722
+ providers_serializers.CarrierConnectionModelSerializer.map(
723
+ data=providers_serializers.CarrierConnectionData.map(data=data).data,
724
+ context=info.context.request,
725
+ )
726
+ .save()
727
+ .instance
728
+ )
729
+
730
+ return CreateCarrierConnectionMutation( # type:ignore
731
+ connection=connection
732
+ )
733
+
734
+
735
+ @strawberry.type
736
+ class UpdateCarrierConnectionMutation(utils.BaseMutation):
737
+ connection: types.CarrierConnectionType = None
738
+
739
+ @staticmethod
740
+ @utils.error_wrapper
741
+ @utils.authentication_required
742
+ @utils.authorization_required(["manage_carriers"])
743
+ def mutate(info: Info, **input) -> "UpdateCarrierConnectionMutation":
744
+ data = input.copy()
745
+ id = data.get("id")
746
+ instance = providers.Carrier.access_by(info.context.request).get(id=id)
747
+ connection = lib.identity(
748
+ providers_serializers.CarrierConnectionModelSerializer.map(
749
+ instance,
750
+ data=data,
751
+ context=info.context.request,
752
+ )
753
+ .save()
754
+ .instance
755
+ )
756
+
757
+ return UpdateCarrierConnectionMutation( # type:ignore
758
+ connection=connection
759
+ )
760
+
761
+
762
+ @strawberry.type
763
+ class SystemCarrierMutation(utils.BaseMutation):
764
+ carrier: typing.Optional[types.SystemConnectionType] = None
765
+
766
+ @staticmethod
767
+ @utils.error_wrapper
768
+ @utils.authentication_required
769
+ @utils.authorization_required(["manage_carriers"])
770
+ def mutate(
771
+ info: Info, **input: inputs.SystemCarrierMutationInput
772
+ ) -> "SystemCarrierMutation":
773
+ pk = input.get("id")
774
+ context = info.context.request
775
+ carrier = providers.Carrier.system_carriers.get(pk=pk)
776
+
777
+ if "enable" in input:
778
+ if input.get("enable"):
779
+ if hasattr(carrier, "active_orgs"):
780
+ carrier.active_orgs.add(info.context.request.org)
781
+ else:
782
+ carrier.active_users.add(info.context.request.user)
783
+ else:
784
+ if hasattr(carrier, "active_orgs"):
785
+ carrier.active_orgs.remove(info.context.request.org)
786
+ else:
787
+ carrier.active_users.remove(info.context.request.user)
788
+
789
+ if "config" in input:
790
+ config = providers.Carrier.resolve_config(carrier, is_user_config=True)
791
+ serializers.CarrierConfigModelSerializer.map(
792
+ instance=config,
793
+ context=context,
794
+ data={
795
+ "carrier": carrier.pk,
796
+ "config": process_dictionaries_mutations(
797
+ ["config"], (input["config"] or {}), config
798
+ ),
799
+ },
800
+ ).save()
801
+
802
+ return SystemCarrierMutation(
803
+ carrier=providers.Carrier.system_carriers.get(pk=pk)
804
+ ) # type: ignore
805
+
806
+
807
+ @strawberry.type
808
+ class UpdateServiceZoneMutation(utils.BaseMutation):
809
+ rate_sheet: typing.Optional[types.RateSheetType] = None
810
+
811
+ @staticmethod
812
+ @transaction.atomic
813
+ @utils.authentication_required
814
+ @utils.authorization_required(["manage_carriers"])
815
+ def mutate(
816
+ info: Info, **input: inputs.UpdateServiceZoneMutationInput
817
+ ) -> "UpdateServiceZoneMutation":
818
+ rate_sheet = providers.RateSheet.access_by(info.context.request).get(
819
+ id=input["id"]
820
+ )
821
+ service = rate_sheet.services.get(id=input["service_id"])
822
+
823
+ serializer = serializers.ServiceLevelModelSerializer(
824
+ service,
825
+ context=info.context.request,
826
+ )
827
+ serializer.update_zone(input["zone_index"], input["zone"])
828
+
829
+ return UpdateServiceZoneMutation(rate_sheet=rate_sheet) # type:ignore
830
+
831
+
832
+ @strawberry.type
833
+ class CreateMetafieldMutation(utils.BaseMutation):
834
+ metafield: typing.Optional[types.MetafieldType] = None
835
+
836
+ @staticmethod
837
+ @utils.authentication_required
838
+ @utils.authorization_required()
839
+ def mutate(
840
+ info: Info, **input: inputs.CreateMetafieldInput
841
+ ) -> "CreateMetafieldMutation":
842
+ data = input.copy()
843
+
844
+ metafield = serializers.MetafieldModelSerializer.map(
845
+ data=data,
846
+ context=info.context.request,
847
+ ).save().instance
848
+
849
+ return CreateMetafieldMutation(metafield=metafield)
850
+
851
+
852
+ @strawberry.type
853
+ class UpdateMetafieldMutation(utils.BaseMutation):
854
+ metafield: typing.Optional[types.MetafieldType] = None
855
+
856
+ @staticmethod
857
+ @utils.authentication_required
858
+ @utils.authorization_required()
859
+ def mutate(
860
+ info: Info, **input: inputs.UpdateMetafieldInput
861
+ ) -> "UpdateMetafieldMutation":
862
+ data = input.copy()
863
+ instance = core.Metafield.access_by(info.context.request).get(id=data.get("id"))
864
+
865
+ metafield = serializers.MetafieldModelSerializer.map(
866
+ instance,
867
+ data=data,
868
+ context=info.context.request,
869
+ ).save().instance
870
+
871
+ return UpdateMetafieldMutation(metafield=metafield)