karrio-server-graph 2026.1.4__py3-none-any.whl → 2026.1.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.
- karrio/server/graph/schemas/base/__init__.py +22 -0
- karrio/server/graph/schemas/base/inputs.py +40 -0
- karrio/server/graph/schemas/base/mutations.py +80 -0
- karrio/server/graph/schemas/base/types.py +14 -0
- karrio/server/graph/tests/test_rate_sheets.py +428 -0
- {karrio_server_graph-2026.1.4.dist-info → karrio_server_graph-2026.1.5.dist-info}/METADATA +1 -1
- {karrio_server_graph-2026.1.4.dist-info → karrio_server_graph-2026.1.5.dist-info}/RECORD +9 -9
- {karrio_server_graph-2026.1.4.dist-info → karrio_server_graph-2026.1.5.dist-info}/WHEEL +0 -0
- {karrio_server_graph-2026.1.4.dist-info → karrio_server_graph-2026.1.5.dist-info}/top_level.txt +0 -0
|
@@ -432,6 +432,28 @@ class Mutation:
|
|
|
432
432
|
) -> mutations.BatchUpdateServiceRatesMutation:
|
|
433
433
|
return mutations.BatchUpdateServiceRatesMutation.mutate(info, **input.to_dict())
|
|
434
434
|
|
|
435
|
+
# ─────────────────────────────────────────────────────────────────
|
|
436
|
+
# WEIGHT RANGE MUTATIONS
|
|
437
|
+
# ─────────────────────────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
@strawberry.mutation
|
|
440
|
+
def add_weight_range(
|
|
441
|
+
self, info: Info, input: inputs.AddWeightRangeMutationInput
|
|
442
|
+
) -> mutations.AddWeightRangeMutation:
|
|
443
|
+
return mutations.AddWeightRangeMutation.mutate(info, **input.to_dict())
|
|
444
|
+
|
|
445
|
+
@strawberry.mutation
|
|
446
|
+
def remove_weight_range(
|
|
447
|
+
self, info: Info, input: inputs.RemoveWeightRangeMutationInput
|
|
448
|
+
) -> mutations.RemoveWeightRangeMutation:
|
|
449
|
+
return mutations.RemoveWeightRangeMutation.mutate(info, **input.to_dict())
|
|
450
|
+
|
|
451
|
+
@strawberry.mutation
|
|
452
|
+
def delete_service_rate(
|
|
453
|
+
self, info: Info, input: inputs.DeleteServiceRateMutationInput
|
|
454
|
+
) -> mutations.DeleteServiceRateMutation:
|
|
455
|
+
return mutations.DeleteServiceRateMutation.mutate(info, **input.to_dict())
|
|
456
|
+
|
|
435
457
|
# ─────────────────────────────────────────────────────────────────
|
|
436
458
|
# SERVICE ZONE/SURCHARGE ASSIGNMENT MUTATIONS
|
|
437
459
|
# ─────────────────────────────────────────────────────────────────
|
|
@@ -71,6 +71,7 @@ class ManifestFilter(utils.Paginated):
|
|
|
71
71
|
class PickupFilter(utils.Paginated):
|
|
72
72
|
keyword: typing.Optional[str] = strawberry.UNSET
|
|
73
73
|
id: typing.Optional[typing.List[str]] = strawberry.UNSET
|
|
74
|
+
status: typing.Optional[typing.List[str]] = strawberry.UNSET
|
|
74
75
|
confirmation_number: typing.Optional[str] = strawberry.UNSET
|
|
75
76
|
pickup_date_after: typing.Optional[str] = strawberry.UNSET
|
|
76
77
|
pickup_date_before: typing.Optional[str] = strawberry.UNSET
|
|
@@ -194,6 +195,11 @@ class WorkspaceConfigMutationInput(utils.BaseInput):
|
|
|
194
195
|
pref_signature_required: typing.Optional[bool] = strawberry.UNSET
|
|
195
196
|
pref_max_lead_time_days: typing.Optional[int] = strawberry.UNSET
|
|
196
197
|
|
|
198
|
+
# ─────────────────────────────────────────────────────────────────
|
|
199
|
+
# Workspace Settings
|
|
200
|
+
# ─────────────────────────────────────────────────────────────────
|
|
201
|
+
shipping_app_test_mode: typing.Optional[bool] = strawberry.UNSET
|
|
202
|
+
|
|
197
203
|
|
|
198
204
|
@strawberry.input
|
|
199
205
|
class TokenMutationInput(utils.BaseInput):
|
|
@@ -937,6 +943,40 @@ class BatchUpdateServiceRatesMutationInput(utils.BaseInput):
|
|
|
937
943
|
rates: typing.List[ServiceRateInput]
|
|
938
944
|
|
|
939
945
|
|
|
946
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
947
|
+
# WEIGHT RANGE INPUTS
|
|
948
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
949
|
+
|
|
950
|
+
|
|
951
|
+
@strawberry.input
|
|
952
|
+
class AddWeightRangeMutationInput(utils.BaseInput):
|
|
953
|
+
"""Add a weight range to a rate sheet (creates rate entries for all service+zone combos)."""
|
|
954
|
+
|
|
955
|
+
rate_sheet_id: str
|
|
956
|
+
min_weight: float
|
|
957
|
+
max_weight: float
|
|
958
|
+
|
|
959
|
+
|
|
960
|
+
@strawberry.input
|
|
961
|
+
class RemoveWeightRangeMutationInput(utils.BaseInput):
|
|
962
|
+
"""Remove a weight range and all its associated rate entries."""
|
|
963
|
+
|
|
964
|
+
rate_sheet_id: str
|
|
965
|
+
min_weight: float
|
|
966
|
+
max_weight: float
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
@strawberry.input
|
|
970
|
+
class DeleteServiceRateMutationInput(utils.BaseInput):
|
|
971
|
+
"""Delete a specific service rate entry."""
|
|
972
|
+
|
|
973
|
+
rate_sheet_id: str
|
|
974
|
+
service_id: str
|
|
975
|
+
zone_id: str
|
|
976
|
+
min_weight: typing.Optional[float] = strawberry.UNSET
|
|
977
|
+
max_weight: typing.Optional[float] = strawberry.UNSET
|
|
978
|
+
|
|
979
|
+
|
|
940
980
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
941
981
|
# SERVICE ZONE/SURCHARGE ASSIGNMENT INPUTS
|
|
942
982
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -882,6 +882,86 @@ class BatchUpdateServiceRatesMutation(utils.BaseMutation):
|
|
|
882
882
|
return BatchUpdateServiceRatesMutation(rate_sheet=rate_sheet)
|
|
883
883
|
|
|
884
884
|
|
|
885
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
886
|
+
# WEIGHT RANGE MUTATIONS
|
|
887
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
@strawberry.type
|
|
891
|
+
class AddWeightRangeMutation(utils.BaseMutation):
|
|
892
|
+
rate_sheet: typing.Optional[types.RateSheetType] = None
|
|
893
|
+
|
|
894
|
+
@staticmethod
|
|
895
|
+
@transaction.atomic
|
|
896
|
+
@utils.authentication_required
|
|
897
|
+
def mutate(
|
|
898
|
+
info: Info, **input: inputs.AddWeightRangeMutationInput
|
|
899
|
+
) -> "AddWeightRangeMutation":
|
|
900
|
+
rate_sheet = providers.RateSheet.access_by(info.context.request).get(
|
|
901
|
+
id=input["rate_sheet_id"]
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
rate_sheet.add_weight_range(
|
|
905
|
+
min_weight=input["min_weight"],
|
|
906
|
+
max_weight=input["max_weight"],
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
return AddWeightRangeMutation(rate_sheet=rate_sheet)
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
@strawberry.type
|
|
913
|
+
class RemoveWeightRangeMutation(utils.BaseMutation):
|
|
914
|
+
rate_sheet: typing.Optional[types.RateSheetType] = None
|
|
915
|
+
|
|
916
|
+
@staticmethod
|
|
917
|
+
@transaction.atomic
|
|
918
|
+
@utils.authentication_required
|
|
919
|
+
def mutate(
|
|
920
|
+
info: Info, **input: inputs.RemoveWeightRangeMutationInput
|
|
921
|
+
) -> "RemoveWeightRangeMutation":
|
|
922
|
+
rate_sheet = providers.RateSheet.access_by(info.context.request).get(
|
|
923
|
+
id=input["rate_sheet_id"]
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
rate_sheet.remove_weight_range(
|
|
927
|
+
min_weight=input["min_weight"],
|
|
928
|
+
max_weight=input["max_weight"],
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
return RemoveWeightRangeMutation(rate_sheet=rate_sheet)
|
|
932
|
+
|
|
933
|
+
|
|
934
|
+
@strawberry.type
|
|
935
|
+
class DeleteServiceRateMutation(utils.BaseMutation):
|
|
936
|
+
rate_sheet: typing.Optional[types.RateSheetType] = None
|
|
937
|
+
|
|
938
|
+
@staticmethod
|
|
939
|
+
@transaction.atomic
|
|
940
|
+
@utils.authentication_required
|
|
941
|
+
def mutate(
|
|
942
|
+
info: Info, **input: inputs.DeleteServiceRateMutationInput
|
|
943
|
+
) -> "DeleteServiceRateMutation":
|
|
944
|
+
rate_sheet = providers.RateSheet.access_by(info.context.request).get(
|
|
945
|
+
id=input["rate_sheet_id"]
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
min_weight = input.get("min_weight")
|
|
949
|
+
max_weight = input.get("max_weight")
|
|
950
|
+
if utils.is_unset(min_weight):
|
|
951
|
+
min_weight = None
|
|
952
|
+
if utils.is_unset(max_weight):
|
|
953
|
+
max_weight = None
|
|
954
|
+
|
|
955
|
+
rate_sheet.remove_service_rate(
|
|
956
|
+
service_id=input["service_id"],
|
|
957
|
+
zone_id=input["zone_id"],
|
|
958
|
+
min_weight=min_weight,
|
|
959
|
+
max_weight=max_weight,
|
|
960
|
+
)
|
|
961
|
+
|
|
962
|
+
return DeleteServiceRateMutation(rate_sheet=rate_sheet)
|
|
963
|
+
|
|
964
|
+
|
|
885
965
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
886
966
|
# SERVICE ZONE/SURCHARGE ASSIGNMENT MUTATIONS
|
|
887
967
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -349,6 +349,19 @@ class WorkspaceConfigType:
|
|
|
349
349
|
|
|
350
350
|
# endregion
|
|
351
351
|
|
|
352
|
+
# ─────────────────────────────────────────────────────────────────
|
|
353
|
+
# Workspace Settings
|
|
354
|
+
# ─────────────────────────────────────────────────────────────────
|
|
355
|
+
# region
|
|
356
|
+
|
|
357
|
+
@strawberry.field
|
|
358
|
+
def shipping_app_test_mode(
|
|
359
|
+
self: auth.WorkspaceConfig,
|
|
360
|
+
) -> typing.Optional[bool]:
|
|
361
|
+
return self.config.get("shipping_app_test_mode")
|
|
362
|
+
|
|
363
|
+
# endregion
|
|
364
|
+
|
|
352
365
|
@staticmethod
|
|
353
366
|
@utils.authentication_required
|
|
354
367
|
def resolve(info) -> typing.Optional["WorkspaceConfigType"]:
|
|
@@ -1520,6 +1533,7 @@ class PickupType:
|
|
|
1520
1533
|
id: str
|
|
1521
1534
|
object_type: str
|
|
1522
1535
|
confirmation_number: typing.Optional[str]
|
|
1536
|
+
status: str
|
|
1523
1537
|
pickup_date: typing.Optional[str]
|
|
1524
1538
|
ready_time: typing.Optional[str]
|
|
1525
1539
|
closing_time: typing.Optional[str]
|
|
@@ -2199,6 +2199,434 @@ UPDATE_SERVICE_ZONE_IDS_RESPONSE = {
|
|
|
2199
2199
|
}
|
|
2200
2200
|
}
|
|
2201
2201
|
|
|
2202
|
+
class TestPerServiceWeightRangeScenarios(GraphTestCase):
|
|
2203
|
+
"""Tests for per-service weight range interactions.
|
|
2204
|
+
|
|
2205
|
+
These tests verify the correct scoping behavior of weight range operations:
|
|
2206
|
+
- Global operations (add_weight_range, remove_weight_range) affect ALL services
|
|
2207
|
+
- Per-service operations (update_service_rate, delete_service_rate) affect ONE service
|
|
2208
|
+
- Editing a weight range on one service must NOT affect other services
|
|
2209
|
+
- Deleting a weight range from one service must NOT affect other services
|
|
2210
|
+
|
|
2211
|
+
Scenarios covered:
|
|
2212
|
+
─────────────────────────────────────────────────────────────────────────────
|
|
2213
|
+
SCENARIO 1: add_weight_range creates rows for ALL services (global)
|
|
2214
|
+
SCENARIO 2: remove_weight_range removes from ALL services (global)
|
|
2215
|
+
SCENARIO 3: delete_service_rate removes from ONE service only
|
|
2216
|
+
SCENARIO 4: Per-service weight range edit (delete old + create new) scoped correctly
|
|
2217
|
+
SCENARIO 5: Per-service weight range delete does not affect other services
|
|
2218
|
+
SCENARIO 6: Services can have different weight ranges independently
|
|
2219
|
+
SCENARIO 7: update_service_rate creates a new entry (implicit add for one service)
|
|
2220
|
+
SCENARIO 8: get_weight_ranges derives from ALL services' rates (union)
|
|
2221
|
+
SCENARIO 9: Overlapping weight range validation on add_weight_range
|
|
2222
|
+
SCENARIO 10: Weight range with negative/invalid values rejected
|
|
2223
|
+
"""
|
|
2224
|
+
|
|
2225
|
+
def setUp(self):
|
|
2226
|
+
super().setUp()
|
|
2227
|
+
|
|
2228
|
+
self.rate_sheet = providers.RateSheet.objects.create(
|
|
2229
|
+
name="Weight Range Scoping Test",
|
|
2230
|
+
carrier_name="dhl_germany",
|
|
2231
|
+
slug="wr_scoping_test",
|
|
2232
|
+
zones=[
|
|
2233
|
+
{"id": "zone_de", "label": "Germany", "country_codes": ["DE"]},
|
|
2234
|
+
{"id": "zone_eu", "label": "EU", "country_codes": ["FR", "IT", "ES"]},
|
|
2235
|
+
],
|
|
2236
|
+
service_rates=[],
|
|
2237
|
+
created_by=self.user,
|
|
2238
|
+
)
|
|
2239
|
+
|
|
2240
|
+
self.svc_paket = providers.ServiceLevel.objects.create(
|
|
2241
|
+
service_name="DHL Paket",
|
|
2242
|
+
service_code="dhl_paket",
|
|
2243
|
+
carrier_service_code="V01PAK",
|
|
2244
|
+
currency="EUR",
|
|
2245
|
+
zone_ids=["zone_de"],
|
|
2246
|
+
created_by=self.user,
|
|
2247
|
+
)
|
|
2248
|
+
self.svc_kleinpaket = providers.ServiceLevel.objects.create(
|
|
2249
|
+
service_name="DHL Kleinpaket",
|
|
2250
|
+
service_code="dhl_kleinpaket",
|
|
2251
|
+
carrier_service_code="V62WP",
|
|
2252
|
+
currency="EUR",
|
|
2253
|
+
zone_ids=["zone_de"],
|
|
2254
|
+
created_by=self.user,
|
|
2255
|
+
)
|
|
2256
|
+
self.rate_sheet.services.add(self.svc_paket, self.svc_kleinpaket)
|
|
2257
|
+
|
|
2258
|
+
# Seed: Paket has 3 weight ranges, Kleinpaket has 2 (different set)
|
|
2259
|
+
self.rate_sheet.service_rates = [
|
|
2260
|
+
# DHL Paket: 0-1, 1-5, 5-10
|
|
2261
|
+
{"service_id": self.svc_paket.id, "zone_id": "zone_de", "rate": 3.99, "min_weight": 0, "max_weight": 1},
|
|
2262
|
+
{"service_id": self.svc_paket.id, "zone_id": "zone_de", "rate": 5.49, "min_weight": 1, "max_weight": 5},
|
|
2263
|
+
{"service_id": self.svc_paket.id, "zone_id": "zone_de", "rate": 8.99, "min_weight": 5, "max_weight": 10},
|
|
2264
|
+
# DHL Kleinpaket: 0-0.5, 0.5-1
|
|
2265
|
+
{"service_id": self.svc_kleinpaket.id, "zone_id": "zone_de", "rate": 2.49, "min_weight": 0, "max_weight": 0.5},
|
|
2266
|
+
{"service_id": self.svc_kleinpaket.id, "zone_id": "zone_de", "rate": 3.39, "min_weight": 0.5, "max_weight": 1},
|
|
2267
|
+
]
|
|
2268
|
+
self.rate_sheet.save()
|
|
2269
|
+
|
|
2270
|
+
# =========================================================================
|
|
2271
|
+
# SCENARIO 1: add_weight_range is GLOBAL (creates for all services)
|
|
2272
|
+
# =========================================================================
|
|
2273
|
+
|
|
2274
|
+
def test_add_weight_range_creates_for_all_services(self):
|
|
2275
|
+
"""add_weight_range creates rate=0 entries for ALL service+zone combos."""
|
|
2276
|
+
self.rate_sheet.add_weight_range(min_weight=10, max_weight=20)
|
|
2277
|
+
self.rate_sheet.refresh_from_db()
|
|
2278
|
+
|
|
2279
|
+
print(self.rate_sheet.service_rates)
|
|
2280
|
+
|
|
2281
|
+
# Both services should get new entries
|
|
2282
|
+
paket_new = [
|
|
2283
|
+
r for r in self.rate_sheet.service_rates
|
|
2284
|
+
if r["service_id"] == self.svc_paket.id and r["min_weight"] == 10
|
|
2285
|
+
]
|
|
2286
|
+
klein_new = [
|
|
2287
|
+
r for r in self.rate_sheet.service_rates
|
|
2288
|
+
if r["service_id"] == self.svc_kleinpaket.id and r["min_weight"] == 10
|
|
2289
|
+
]
|
|
2290
|
+
self.assertEqual(len(paket_new), 1, "Paket should get the new weight range")
|
|
2291
|
+
self.assertEqual(len(klein_new), 1, "Kleinpaket should get the new weight range")
|
|
2292
|
+
self.assertEqual(paket_new[0]["rate"], 0, "New entries should have rate=0")
|
|
2293
|
+
self.assertEqual(klein_new[0]["rate"], 0, "New entries should have rate=0")
|
|
2294
|
+
|
|
2295
|
+
# =========================================================================
|
|
2296
|
+
# SCENARIO 2: remove_weight_range is GLOBAL (removes from all services)
|
|
2297
|
+
# =========================================================================
|
|
2298
|
+
|
|
2299
|
+
def test_remove_weight_range_removes_from_all_services(self):
|
|
2300
|
+
"""remove_weight_range deletes matching entries from ALL services."""
|
|
2301
|
+
# Add a shared weight range first
|
|
2302
|
+
self.rate_sheet.add_weight_range(min_weight=10, max_weight=20)
|
|
2303
|
+
self.rate_sheet.refresh_from_db()
|
|
2304
|
+
total_before = len(self.rate_sheet.service_rates)
|
|
2305
|
+
|
|
2306
|
+
# Remove it globally
|
|
2307
|
+
self.rate_sheet.remove_weight_range(min_weight=10, max_weight=20)
|
|
2308
|
+
self.rate_sheet.refresh_from_db()
|
|
2309
|
+
|
|
2310
|
+
print(self.rate_sheet.service_rates)
|
|
2311
|
+
|
|
2312
|
+
remaining = [
|
|
2313
|
+
r for r in self.rate_sheet.service_rates
|
|
2314
|
+
if r.get("min_weight") == 10 and r.get("max_weight") == 20
|
|
2315
|
+
]
|
|
2316
|
+
self.assertEqual(len(remaining), 0, "All entries with 10-20 should be gone")
|
|
2317
|
+
# Original entries still intact
|
|
2318
|
+
self.assertEqual(len(self.rate_sheet.service_rates), 5, "Original 5 entries should remain")
|
|
2319
|
+
|
|
2320
|
+
# =========================================================================
|
|
2321
|
+
# SCENARIO 3: delete_service_rate removes from ONE service only
|
|
2322
|
+
# =========================================================================
|
|
2323
|
+
|
|
2324
|
+
def test_delete_service_rate_scoped_to_one_service(self):
|
|
2325
|
+
"""remove_service_rate with weight bracket only affects the specified service."""
|
|
2326
|
+
# Add a shared weight range (both services get it)
|
|
2327
|
+
self.rate_sheet.add_weight_range(min_weight=10, max_weight=20)
|
|
2328
|
+
self.rate_sheet.refresh_from_db()
|
|
2329
|
+
|
|
2330
|
+
# Delete only from Kleinpaket
|
|
2331
|
+
self.rate_sheet.remove_service_rate(
|
|
2332
|
+
service_id=self.svc_kleinpaket.id,
|
|
2333
|
+
zone_id="zone_de",
|
|
2334
|
+
min_weight=10,
|
|
2335
|
+
max_weight=20,
|
|
2336
|
+
)
|
|
2337
|
+
self.rate_sheet.refresh_from_db()
|
|
2338
|
+
|
|
2339
|
+
print(self.rate_sheet.service_rates)
|
|
2340
|
+
|
|
2341
|
+
# Paket should still have it
|
|
2342
|
+
paket_remaining = [
|
|
2343
|
+
r for r in self.rate_sheet.service_rates
|
|
2344
|
+
if r["service_id"] == self.svc_paket.id and r.get("min_weight") == 10
|
|
2345
|
+
]
|
|
2346
|
+
klein_remaining = [
|
|
2347
|
+
r for r in self.rate_sheet.service_rates
|
|
2348
|
+
if r["service_id"] == self.svc_kleinpaket.id and r.get("min_weight") == 10
|
|
2349
|
+
]
|
|
2350
|
+
self.assertEqual(len(paket_remaining), 1, "Paket still has 10-20")
|
|
2351
|
+
self.assertEqual(len(klein_remaining), 0, "Kleinpaket 10-20 was removed")
|
|
2352
|
+
|
|
2353
|
+
# =========================================================================
|
|
2354
|
+
# SCENARIO 4: Per-service weight range edit (delete old + create new)
|
|
2355
|
+
# =========================================================================
|
|
2356
|
+
|
|
2357
|
+
def test_per_service_weight_range_edit_does_not_affect_other_services(self):
|
|
2358
|
+
"""Editing a weight range on one service (delete old + add new) should NOT
|
|
2359
|
+
create rows on other services. This simulates the frontend's per-service
|
|
2360
|
+
EditWeightRangeDialog flow: deleteServiceRate + updateServiceRate."""
|
|
2361
|
+
|
|
2362
|
+
# Edit Paket's 5-10 range to 5-15 (per-service, NOT global)
|
|
2363
|
+
old_rate = next(
|
|
2364
|
+
r for r in self.rate_sheet.service_rates
|
|
2365
|
+
if r["service_id"] == self.svc_paket.id and r["min_weight"] == 5 and r["max_weight"] == 10
|
|
2366
|
+
)
|
|
2367
|
+
|
|
2368
|
+
# Step 1: Delete old entry (per-service)
|
|
2369
|
+
self.rate_sheet.remove_service_rate(
|
|
2370
|
+
service_id=self.svc_paket.id,
|
|
2371
|
+
zone_id="zone_de",
|
|
2372
|
+
min_weight=5,
|
|
2373
|
+
max_weight=10,
|
|
2374
|
+
)
|
|
2375
|
+
|
|
2376
|
+
# Step 2: Create new entry with updated max_weight (per-service)
|
|
2377
|
+
self.rate_sheet.update_service_rate(
|
|
2378
|
+
service_id=self.svc_paket.id,
|
|
2379
|
+
zone_id="zone_de",
|
|
2380
|
+
rate_data={"rate": old_rate["rate"], "min_weight": 5, "max_weight": 15},
|
|
2381
|
+
)
|
|
2382
|
+
self.rate_sheet.refresh_from_db()
|
|
2383
|
+
|
|
2384
|
+
print(self.rate_sheet.service_rates)
|
|
2385
|
+
|
|
2386
|
+
# Verify Paket has new range
|
|
2387
|
+
paket_rates = [r for r in self.rate_sheet.service_rates if r["service_id"] == self.svc_paket.id]
|
|
2388
|
+
paket_weights = [(r["min_weight"], r["max_weight"]) for r in paket_rates]
|
|
2389
|
+
self.assertIn((5, 15), paket_weights, "Paket should have 5-15")
|
|
2390
|
+
self.assertNotIn((5, 10), paket_weights, "Paket should NOT have 5-10 anymore")
|
|
2391
|
+
|
|
2392
|
+
# Verify Kleinpaket is UNCHANGED
|
|
2393
|
+
klein_rates = [r for r in self.rate_sheet.service_rates if r["service_id"] == self.svc_kleinpaket.id]
|
|
2394
|
+
klein_weights = [(r["min_weight"], r["max_weight"]) for r in klein_rates]
|
|
2395
|
+
self.assertEqual(
|
|
2396
|
+
sorted(klein_weights),
|
|
2397
|
+
[(0, 0.5), (0.5, 1)],
|
|
2398
|
+
"Kleinpaket should be completely unaffected",
|
|
2399
|
+
)
|
|
2400
|
+
|
|
2401
|
+
# Total entries should be same (3 Paket + 2 Kleinpaket = 5)
|
|
2402
|
+
self.assertEqual(len(self.rate_sheet.service_rates), 5)
|
|
2403
|
+
|
|
2404
|
+
# =========================================================================
|
|
2405
|
+
# SCENARIO 5: Per-service weight range delete does NOT affect others
|
|
2406
|
+
# =========================================================================
|
|
2407
|
+
|
|
2408
|
+
def test_per_service_weight_range_delete_does_not_affect_others(self):
|
|
2409
|
+
"""Deleting a weight range from one service by removing all its zone rates
|
|
2410
|
+
should NOT affect other services' rates for different weight ranges."""
|
|
2411
|
+
|
|
2412
|
+
# Delete all Paket's 5-10 entries (simulate removing a weight range row)
|
|
2413
|
+
self.rate_sheet.remove_service_rate(
|
|
2414
|
+
service_id=self.svc_paket.id,
|
|
2415
|
+
zone_id="zone_de",
|
|
2416
|
+
min_weight=5,
|
|
2417
|
+
max_weight=10,
|
|
2418
|
+
)
|
|
2419
|
+
self.rate_sheet.refresh_from_db()
|
|
2420
|
+
|
|
2421
|
+
print(self.rate_sheet.service_rates)
|
|
2422
|
+
|
|
2423
|
+
# Paket should have 2 ranges now
|
|
2424
|
+
paket_rates = [r for r in self.rate_sheet.service_rates if r["service_id"] == self.svc_paket.id]
|
|
2425
|
+
self.assertEqual(len(paket_rates), 2, "Paket should have 2 ranges left")
|
|
2426
|
+
|
|
2427
|
+
# Kleinpaket unchanged
|
|
2428
|
+
klein_rates = [r for r in self.rate_sheet.service_rates if r["service_id"] == self.svc_kleinpaket.id]
|
|
2429
|
+
self.assertEqual(len(klein_rates), 2, "Kleinpaket should still have 2 ranges")
|
|
2430
|
+
|
|
2431
|
+
# =========================================================================
|
|
2432
|
+
# SCENARIO 6: Services can have different weight ranges independently
|
|
2433
|
+
# =========================================================================
|
|
2434
|
+
|
|
2435
|
+
def test_services_have_independent_weight_ranges(self):
|
|
2436
|
+
"""Each service can have its own set of weight ranges that don't overlap
|
|
2437
|
+
with other services."""
|
|
2438
|
+
|
|
2439
|
+
paket_ranges = set()
|
|
2440
|
+
klein_ranges = set()
|
|
2441
|
+
for r in self.rate_sheet.service_rates:
|
|
2442
|
+
key = (r["min_weight"], r["max_weight"])
|
|
2443
|
+
if r["service_id"] == self.svc_paket.id:
|
|
2444
|
+
paket_ranges.add(key)
|
|
2445
|
+
else:
|
|
2446
|
+
klein_ranges.add(key)
|
|
2447
|
+
|
|
2448
|
+
print(f"Paket ranges: {paket_ranges}")
|
|
2449
|
+
print(f"Klein ranges: {klein_ranges}")
|
|
2450
|
+
|
|
2451
|
+
self.assertEqual(paket_ranges, {(0, 1), (1, 5), (5, 10)})
|
|
2452
|
+
self.assertEqual(klein_ranges, {(0, 0.5), (0.5, 1)})
|
|
2453
|
+
# They are different sets
|
|
2454
|
+
self.assertNotEqual(paket_ranges, klein_ranges)
|
|
2455
|
+
|
|
2456
|
+
# =========================================================================
|
|
2457
|
+
# SCENARIO 7: update_service_rate creates implicit per-service entry
|
|
2458
|
+
# =========================================================================
|
|
2459
|
+
|
|
2460
|
+
def test_update_service_rate_creates_entry_for_single_service(self):
|
|
2461
|
+
"""update_service_rate can create a new weight bracket for a single service
|
|
2462
|
+
without affecting other services."""
|
|
2463
|
+
|
|
2464
|
+
# Add a 10-20 range to Kleinpaket only (via update_service_rate)
|
|
2465
|
+
self.rate_sheet.update_service_rate(
|
|
2466
|
+
service_id=self.svc_kleinpaket.id,
|
|
2467
|
+
zone_id="zone_de",
|
|
2468
|
+
rate_data={"rate": 12.50, "min_weight": 10, "max_weight": 20},
|
|
2469
|
+
)
|
|
2470
|
+
self.rate_sheet.refresh_from_db()
|
|
2471
|
+
|
|
2472
|
+
print(self.rate_sheet.service_rates)
|
|
2473
|
+
|
|
2474
|
+
# Kleinpaket now has 3 ranges
|
|
2475
|
+
klein_rates = [r for r in self.rate_sheet.service_rates if r["service_id"] == self.svc_kleinpaket.id]
|
|
2476
|
+
self.assertEqual(len(klein_rates), 3)
|
|
2477
|
+
new_rate = next(r for r in klein_rates if r["min_weight"] == 10)
|
|
2478
|
+
self.assertEqual(new_rate["rate"], 12.50)
|
|
2479
|
+
|
|
2480
|
+
# Paket should NOT have 10-20
|
|
2481
|
+
paket_has_10_20 = any(
|
|
2482
|
+
r for r in self.rate_sheet.service_rates
|
|
2483
|
+
if r["service_id"] == self.svc_paket.id and r["min_weight"] == 10
|
|
2484
|
+
)
|
|
2485
|
+
self.assertFalse(paket_has_10_20, "Paket should NOT get the 10-20 range")
|
|
2486
|
+
|
|
2487
|
+
# =========================================================================
|
|
2488
|
+
# SCENARIO 8: get_weight_ranges is union across all services
|
|
2489
|
+
# =========================================================================
|
|
2490
|
+
|
|
2491
|
+
def test_get_weight_ranges_union_all_services(self):
|
|
2492
|
+
"""get_weight_ranges returns the union of all weight ranges across services."""
|
|
2493
|
+
ranges = self.rate_sheet.get_weight_ranges()
|
|
2494
|
+
range_tuples = [(r["min_weight"], r["max_weight"]) for r in ranges]
|
|
2495
|
+
|
|
2496
|
+
print(f"All ranges: {range_tuples}")
|
|
2497
|
+
|
|
2498
|
+
# Union of Paket (0-1, 1-5, 5-10) and Kleinpaket (0-0.5, 0.5-1)
|
|
2499
|
+
expected = {(0, 0.5), (0, 1), (0.5, 1), (1, 5), (5, 10)}
|
|
2500
|
+
self.assertEqual(set(range_tuples), expected)
|
|
2501
|
+
|
|
2502
|
+
# =========================================================================
|
|
2503
|
+
# SCENARIO 9: add_weight_range overlap validation
|
|
2504
|
+
# =========================================================================
|
|
2505
|
+
|
|
2506
|
+
def test_add_weight_range_rejects_overlap(self):
|
|
2507
|
+
"""add_weight_range raises ValueError when the new range overlaps existing ones."""
|
|
2508
|
+
with self.assertRaises(ValueError) as ctx:
|
|
2509
|
+
self.rate_sheet.add_weight_range(min_weight=0, max_weight=2)
|
|
2510
|
+
|
|
2511
|
+
print(str(ctx.exception))
|
|
2512
|
+
self.assertIn("overlaps", str(ctx.exception).lower())
|
|
2513
|
+
|
|
2514
|
+
# =========================================================================
|
|
2515
|
+
# SCENARIO 10: Input validation on add_weight_range
|
|
2516
|
+
# =========================================================================
|
|
2517
|
+
|
|
2518
|
+
def test_add_weight_range_rejects_invalid_inputs(self):
|
|
2519
|
+
"""add_weight_range rejects negative min_weight and max <= min."""
|
|
2520
|
+
with self.assertRaises(ValueError):
|
|
2521
|
+
self.rate_sheet.add_weight_range(min_weight=-1, max_weight=5)
|
|
2522
|
+
|
|
2523
|
+
with self.assertRaises(ValueError):
|
|
2524
|
+
self.rate_sheet.add_weight_range(min_weight=5, max_weight=3)
|
|
2525
|
+
|
|
2526
|
+
with self.assertRaises(ValueError):
|
|
2527
|
+
self.rate_sheet.add_weight_range(min_weight=5, max_weight=5)
|
|
2528
|
+
|
|
2529
|
+
# =========================================================================
|
|
2530
|
+
# SCENARIO 11: Multi-zone per-service edit
|
|
2531
|
+
# =========================================================================
|
|
2532
|
+
|
|
2533
|
+
def test_per_service_edit_with_multiple_zones(self):
|
|
2534
|
+
"""When a service has rates in multiple zones, editing a weight range should
|
|
2535
|
+
re-key all zone entries for that service only."""
|
|
2536
|
+
|
|
2537
|
+
# Give Paket a zone_eu entry at 5-10
|
|
2538
|
+
self.rate_sheet.update_service_rate(
|
|
2539
|
+
service_id=self.svc_paket.id,
|
|
2540
|
+
zone_id="zone_eu",
|
|
2541
|
+
rate_data={"rate": 12.99, "min_weight": 5, "max_weight": 10},
|
|
2542
|
+
)
|
|
2543
|
+
self.rate_sheet.refresh_from_db()
|
|
2544
|
+
|
|
2545
|
+
# Now simulate per-service edit: change 5-10 → 5-15 for Paket
|
|
2546
|
+
# Delete old entries for Paket 5-10 (both zones)
|
|
2547
|
+
paket_old = [
|
|
2548
|
+
r for r in self.rate_sheet.service_rates
|
|
2549
|
+
if r["service_id"] == self.svc_paket.id and r["min_weight"] == 5 and r["max_weight"] == 10
|
|
2550
|
+
]
|
|
2551
|
+
for r in paket_old:
|
|
2552
|
+
self.rate_sheet.remove_service_rate(
|
|
2553
|
+
service_id=r["service_id"], zone_id=r["zone_id"],
|
|
2554
|
+
min_weight=5, max_weight=10,
|
|
2555
|
+
)
|
|
2556
|
+
|
|
2557
|
+
# Add new entries with 5-15
|
|
2558
|
+
for r in paket_old:
|
|
2559
|
+
self.rate_sheet.update_service_rate(
|
|
2560
|
+
service_id=r["service_id"], zone_id=r["zone_id"],
|
|
2561
|
+
rate_data={"rate": r["rate"], "min_weight": 5, "max_weight": 15},
|
|
2562
|
+
)
|
|
2563
|
+
self.rate_sheet.refresh_from_db()
|
|
2564
|
+
|
|
2565
|
+
print(self.rate_sheet.service_rates)
|
|
2566
|
+
|
|
2567
|
+
# Paket should have 5-15 in both zones
|
|
2568
|
+
paket_de = next(
|
|
2569
|
+
r for r in self.rate_sheet.service_rates
|
|
2570
|
+
if r["service_id"] == self.svc_paket.id and r["zone_id"] == "zone_de" and r["min_weight"] == 5
|
|
2571
|
+
)
|
|
2572
|
+
paket_eu = next(
|
|
2573
|
+
r for r in self.rate_sheet.service_rates
|
|
2574
|
+
if r["service_id"] == self.svc_paket.id and r["zone_id"] == "zone_eu" and r["min_weight"] == 5
|
|
2575
|
+
)
|
|
2576
|
+
self.assertEqual(paket_de["max_weight"], 15)
|
|
2577
|
+
self.assertEqual(paket_eu["max_weight"], 15)
|
|
2578
|
+
self.assertEqual(paket_de["rate"], 8.99)
|
|
2579
|
+
self.assertEqual(paket_eu["rate"], 12.99)
|
|
2580
|
+
|
|
2581
|
+
# Kleinpaket completely unaffected
|
|
2582
|
+
klein_rates = [r for r in self.rate_sheet.service_rates if r["service_id"] == self.svc_kleinpaket.id]
|
|
2583
|
+
self.assertEqual(len(klein_rates), 2, "Kleinpaket unchanged")
|
|
2584
|
+
|
|
2585
|
+
# =========================================================================
|
|
2586
|
+
# SCENARIO 12: Deleting all zones' rates for a weight range on one service
|
|
2587
|
+
# =========================================================================
|
|
2588
|
+
|
|
2589
|
+
def test_delete_weight_range_from_one_service_all_zones(self):
|
|
2590
|
+
"""When deleting a weight range from a service with multiple zones,
|
|
2591
|
+
each zone's entry must be deleted individually."""
|
|
2592
|
+
|
|
2593
|
+
# Add zone_eu to Paket
|
|
2594
|
+
self.svc_paket.zone_ids = ["zone_de", "zone_eu"]
|
|
2595
|
+
self.svc_paket.save()
|
|
2596
|
+
self.rate_sheet.update_service_rate(
|
|
2597
|
+
service_id=self.svc_paket.id, zone_id="zone_eu",
|
|
2598
|
+
rate_data={"rate": 7.99, "min_weight": 1, "max_weight": 5},
|
|
2599
|
+
)
|
|
2600
|
+
self.rate_sheet.refresh_from_db()
|
|
2601
|
+
|
|
2602
|
+
# Delete Paket's 1-5 range from ALL zones (per-service)
|
|
2603
|
+
paket_1_5 = [
|
|
2604
|
+
r for r in self.rate_sheet.service_rates
|
|
2605
|
+
if r["service_id"] == self.svc_paket.id and r["min_weight"] == 1 and r["max_weight"] == 5
|
|
2606
|
+
]
|
|
2607
|
+
self.assertEqual(len(paket_1_5), 2, "Paket has 1-5 in 2 zones")
|
|
2608
|
+
|
|
2609
|
+
for r in paket_1_5:
|
|
2610
|
+
self.rate_sheet.remove_service_rate(
|
|
2611
|
+
service_id=r["service_id"], zone_id=r["zone_id"],
|
|
2612
|
+
min_weight=1, max_weight=5,
|
|
2613
|
+
)
|
|
2614
|
+
self.rate_sheet.refresh_from_db()
|
|
2615
|
+
|
|
2616
|
+
print(self.rate_sheet.service_rates)
|
|
2617
|
+
|
|
2618
|
+
# Paket should have no 1-5 entries
|
|
2619
|
+
paket_1_5_after = [
|
|
2620
|
+
r for r in self.rate_sheet.service_rates
|
|
2621
|
+
if r["service_id"] == self.svc_paket.id and r["min_weight"] == 1 and r["max_weight"] == 5
|
|
2622
|
+
]
|
|
2623
|
+
self.assertEqual(len(paket_1_5_after), 0)
|
|
2624
|
+
|
|
2625
|
+
# Kleinpaket unchanged
|
|
2626
|
+
klein_rates = [r for r in self.rate_sheet.service_rates if r["service_id"] == self.svc_kleinpaket.id]
|
|
2627
|
+
self.assertEqual(len(klein_rates), 2)
|
|
2628
|
+
|
|
2629
|
+
|
|
2202
2630
|
UPDATE_SERVICE_SURCHARGE_IDS_RESPONSE = {
|
|
2203
2631
|
"data": {
|
|
2204
2632
|
"update_service_surcharge_ids": {
|
|
@@ -16,10 +16,10 @@ karrio/server/graph/migrations/0002_auto_20210512_1353.py,sha256=TnUwR9EP0qp3gJ3
|
|
|
16
16
|
karrio/server/graph/migrations/0003_remove_template_customs.py,sha256=XTfeRtpYi8U7C2G2yyEBmWTkkRf4kZlJizl_iznXeOU,280
|
|
17
17
|
karrio/server/graph/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
18
|
karrio/server/graph/schemas/__init__.py,sha256=Kn-I1j3HP3jwZccpz6FL9r1k6b3UEAMVh2kFPCKNS0w,80
|
|
19
|
-
karrio/server/graph/schemas/base/__init__.py,sha256=
|
|
20
|
-
karrio/server/graph/schemas/base/inputs.py,sha256=
|
|
21
|
-
karrio/server/graph/schemas/base/mutations.py,sha256=
|
|
22
|
-
karrio/server/graph/schemas/base/types.py,sha256=
|
|
19
|
+
karrio/server/graph/schemas/base/__init__.py,sha256=2DqdhEg-TcpDjs-MBeZH8NU5uAL1393madGheJrgdEY,20745
|
|
20
|
+
karrio/server/graph/schemas/base/inputs.py,sha256=1dY4xU78OpHhTlauEK0vt-KN33Fy_HfQX-vY4No4QHc,42486
|
|
21
|
+
karrio/server/graph/schemas/base/mutations.py,sha256=OLCdkDPewPv4W-XrA7jMzDMm67-W5rD8INzrH7uI1QI,52910
|
|
22
|
+
karrio/server/graph/schemas/base/types.py,sha256=K2qcw5ZIT7CKgnSPCa7vnrh7zI9fwejD43_7AVumQEk,81520
|
|
23
23
|
karrio/server/graph/templates/graphql/graphiql.html,sha256=MQjQbBqoRE0QLsOUck8SaXo6B2oJO8dT6YZzUqbDan0,3786
|
|
24
24
|
karrio/server/graph/templates/karrio/email_change_email.html,sha256=gr55F97GYzY27TVKGl49037yd60eSYD0b0GXRlyoco4,552
|
|
25
25
|
karrio/server/graph/templates/karrio/email_change_email.txt,sha256=NXXuzLR63hn2F1fVAjzmuguptuuTvujwqI7YLSrQoio,431
|
|
@@ -30,12 +30,12 @@ karrio/server/graph/tests/test_carrier_connections.py,sha256=JBI_jRfJs9i_GP8fHBU
|
|
|
30
30
|
karrio/server/graph/tests/test_metafield.py,sha256=K7Oon0CLEm_MUMbmcu0t2iAZvFN8Wl7Kp4QAWeUXo_Y,12783
|
|
31
31
|
karrio/server/graph/tests/test_partial_shipments.py,sha256=dPIdIq4wiyovOaIIzbIX69eZnBqCA4ZvBSiGKYADM2g,19031
|
|
32
32
|
karrio/server/graph/tests/test_pickups.py,sha256=65YkOdWUgEXO9Iweo66vobi96VVUV_wCFo8ZmFXiw2I,11004
|
|
33
|
-
karrio/server/graph/tests/test_rate_sheets.py,sha256=
|
|
33
|
+
karrio/server/graph/tests/test_rate_sheets.py,sha256=2SQmzjkLIkFznTuMpmT5TK4oNmyS_o42rZUdPon8zng,93950
|
|
34
34
|
karrio/server/graph/tests/test_registration.py,sha256=0vCTqlsLc0cl2m78umgfm7grnDgTI_NZJWNUrRUlQBY,7107
|
|
35
35
|
karrio/server/graph/tests/test_templates.py,sha256=juHtyN2XxnWPeUv-5sgucKLdXjtJa9_MBbb2-SFW5mM,14188
|
|
36
36
|
karrio/server/graph/tests/test_user_info.py,sha256=K91BL7SgxLWflCZriSVI8Vt5M5RIqmSCHKrgN2w8GmM,1928
|
|
37
37
|
karrio/server/settings/graph.py,sha256=cz2yQHbp3xCfyFKuUkPEFfkI2fFVggExIY49wGz7mt0,106
|
|
38
|
-
karrio_server_graph-2026.1.
|
|
39
|
-
karrio_server_graph-2026.1.
|
|
40
|
-
karrio_server_graph-2026.1.
|
|
41
|
-
karrio_server_graph-2026.1.
|
|
38
|
+
karrio_server_graph-2026.1.5.dist-info/METADATA,sha256=CQDPu_G4pEpLrCyntJF_CuhGjm1PEXpIAD9BGCDvAqg,740
|
|
39
|
+
karrio_server_graph-2026.1.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
40
|
+
karrio_server_graph-2026.1.5.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
|
|
41
|
+
karrio_server_graph-2026.1.5.dist-info/RECORD,,
|
|
File without changes
|
{karrio_server_graph-2026.1.4.dist-info → karrio_server_graph-2026.1.5.dist-info}/top_level.txt
RENAMED
|
File without changes
|