karrio-server-graph 2025.5.4__py3-none-any.whl → 2025.5.6__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.
@@ -1148,15 +1148,18 @@ class ShipmentType:
1148
1148
  return utils.paginated_connection(queryset, **_filter.pagination())
1149
1149
 
1150
1150
 
1151
+ # ─────────────────────────────────────────────────────────────────────────────
1152
+ # SHARED ZONE TYPE (Rate Sheet Level)
1153
+ # ─────────────────────────────────────────────────────────────────────────────
1154
+
1155
+
1151
1156
  @strawberry.type
1152
- class ServiceZoneType:
1157
+ class SharedZoneType:
1158
+ """Shared zone definition at the RateSheet level."""
1159
+
1153
1160
  object_type: str
1154
- id: typing.Optional[str] = None
1161
+ id: str
1155
1162
  label: typing.Optional[str] = None
1156
- rate: typing.Optional[float] = None
1157
-
1158
- min_weight: typing.Optional[float] = None
1159
- max_weight: typing.Optional[float] = None
1160
1163
 
1161
1164
  transit_days: typing.Optional[int] = None
1162
1165
  transit_time: typing.Optional[float] = None
@@ -1167,24 +1170,98 @@ class ServiceZoneType:
1167
1170
 
1168
1171
  cities: typing.Optional[typing.List[str]] = None
1169
1172
  postal_codes: typing.Optional[typing.List[str]] = None
1170
- country_codes: typing.Optional[typing.List[utils.CountryCodeEnum]] = None
1173
+ country_codes: typing.Optional[typing.List[str]] = None
1171
1174
 
1172
1175
  @staticmethod
1173
1176
  def parse(zone: dict):
1174
- return ServiceZoneType(
1175
- **{
1176
- "object_type": "zone",
1177
- **{
1178
- k: v
1179
- for k, v in zone.items()
1180
- if k in ServiceZoneType.__annotations__
1181
- },
1182
- }
1177
+ return SharedZoneType(
1178
+ object_type="shared_zone",
1179
+ id=zone.get("id", ""),
1180
+ label=zone.get("label"),
1181
+ transit_days=zone.get("transit_days"),
1182
+ transit_time=zone.get("transit_time"),
1183
+ radius=zone.get("radius"),
1184
+ latitude=zone.get("latitude"),
1185
+ longitude=zone.get("longitude"),
1186
+ cities=zone.get("cities"),
1187
+ postal_codes=zone.get("postal_codes"),
1188
+ country_codes=zone.get("country_codes"),
1189
+ )
1190
+
1191
+
1192
+ # ─────────────────────────────────────────────────────────────────────────────
1193
+ # SHARED SURCHARGE TYPE (Rate Sheet Level)
1194
+ # ─────────────────────────────────────────────────────────────────────────────
1195
+
1196
+
1197
+ @strawberry.type
1198
+ class SharedSurchargeType:
1199
+ """Shared surcharge definition at the RateSheet level."""
1200
+
1201
+ object_type: str
1202
+ id: str
1203
+ name: str
1204
+ amount: float
1205
+ surcharge_type: str
1206
+ cost: typing.Optional[float] = None
1207
+ active: bool = True
1208
+
1209
+ @staticmethod
1210
+ def parse(surcharge: dict):
1211
+ return SharedSurchargeType(
1212
+ object_type="shared_surcharge",
1213
+ id=surcharge.get("id", ""),
1214
+ name=surcharge.get("name", ""),
1215
+ amount=float(surcharge.get("amount", 0)),
1216
+ surcharge_type=surcharge.get("surcharge_type", "fixed"),
1217
+ cost=surcharge.get("cost"),
1218
+ active=surcharge.get("active", True),
1219
+ )
1220
+
1221
+
1222
+ # ─────────────────────────────────────────────────────────────────────────────
1223
+ # SERVICE RATE TYPE (Service-Zone Rate Mapping)
1224
+ # ─────────────────────────────────────────────────────────────────────────────
1225
+
1226
+
1227
+ @strawberry.type
1228
+ class ServiceRateType:
1229
+ """Service-zone rate mapping."""
1230
+
1231
+ object_type: str
1232
+ service_id: str
1233
+ zone_id: str
1234
+ rate: float
1235
+ cost: typing.Optional[float] = None
1236
+ min_weight: typing.Optional[float] = None
1237
+ max_weight: typing.Optional[float] = None
1238
+ transit_days: typing.Optional[int] = None
1239
+ transit_time: typing.Optional[float] = None
1240
+
1241
+ @staticmethod
1242
+ def parse(rate: dict):
1243
+ return ServiceRateType(
1244
+ object_type="service_rate",
1245
+ service_id=rate.get("service_id", ""),
1246
+ zone_id=rate.get("zone_id", ""),
1247
+ rate=float(rate.get("rate", 0)),
1248
+ cost=rate.get("cost"),
1249
+ min_weight=rate.get("min_weight"),
1250
+ max_weight=rate.get("max_weight"),
1251
+ transit_days=rate.get("transit_days"),
1252
+ transit_time=rate.get("transit_time"),
1183
1253
  )
1184
1254
 
1185
1255
 
1186
1256
  @strawberry.type
1187
1257
  class ServiceLevelType:
1258
+ """
1259
+ Service level definition for rate sheet-based shipping.
1260
+
1261
+ Services reference shared zones and surcharges defined at the RateSheet level
1262
+ via zone_ids and surcharge_ids. Rate values are stored in RateSheet.service_rates.
1263
+ """
1264
+
1188
1265
  id: str
1189
1266
  object_type: str
1190
1267
  service_name: typing.Optional[str]
@@ -1202,24 +1279,33 @@ class ServiceLevelType:
1202
1279
  max_length: typing.Optional[float]
1203
1280
  dimension_unit: typing.Optional[utils.DimensionUnitEnum]
1204
1281
 
1282
+ min_weight: typing.Optional[float]
1205
1283
  max_weight: typing.Optional[float]
1206
1284
  weight_unit: typing.Optional[utils.WeightUnitEnum]
1207
1285
 
1286
+ max_volume: typing.Optional[float]
1287
+ cost: typing.Optional[float]
1288
+
1208
1289
  domicile: typing.Optional[bool]
1209
1290
  international: typing.Optional[bool]
1210
1291
 
1211
1292
  @strawberry.field
1212
- def zones(self: providers.ServiceLevel) -> typing.List[ServiceZoneType]:
1213
- # Use computed_zones for backward compatibility with optimized structure
1214
- return [ServiceZoneType.parse(zone) for zone in self.computed_zones]
1215
-
1216
- @strawberry.field
1217
- def metadata(self: providers.RateSheet) -> typing.Optional[utils.JSON]:
1293
+ def metadata(self: providers.ServiceLevel) -> typing.Optional[utils.JSON]:
1218
1294
  try:
1219
1295
  return lib.to_dict(self.metadata)
1220
1296
  except:
1221
1297
  return self.metadata
1222
1298
 
1299
+ @strawberry.field
1300
+ def zone_ids(self: providers.ServiceLevel) -> typing.List[str]:
1301
+ """List of zone IDs this service applies to (references RateSheet.zones)."""
1302
+ return self.zone_ids or []
1303
+
1304
+ @strawberry.field
1305
+ def surcharge_ids(self: providers.ServiceLevel) -> typing.List[str]:
1306
+ """List of surcharge IDs to apply (references RateSheet.surcharges)."""
1307
+ return self.surcharge_ids or []
1308
+
1223
1309
 
1224
1310
  @strawberry.type
1225
1311
  class LabelTemplateType:
@@ -1256,6 +1342,37 @@ class RateSheetType:
1256
1342
  def services(self: providers.RateSheet) -> typing.List[ServiceLevelType]:
1257
1343
  return self.services.all()
1258
1344
 
1345
+ # ─────────────────────────────────────────────────────────────────
1346
+ # NEW: Shared zones at RateSheet level
1347
+ # ─────────────────────────────────────────────────────────────────
1348
+ @strawberry.field
1349
+ def zones(self: providers.RateSheet) -> typing.Optional[typing.List[SharedZoneType]]:
1350
+ """Shared zone definitions for this rate sheet."""
1351
+ zones_data = self.zones or []
1352
+ return [SharedZoneType.parse(zone) for zone in zones_data]
1353
+
1354
+ # ─────────────────────────────────────────────────────────────────
1355
+ # NEW: Shared surcharges at RateSheet level
1356
+ # ─────────────────────────────────────────────────────────────────
1357
+ @strawberry.field
1358
+ def surcharges(
1359
+ self: providers.RateSheet,
1360
+ ) -> typing.Optional[typing.List[SharedSurchargeType]]:
1361
+ """Shared surcharge definitions for this rate sheet."""
1362
+ surcharges_data = self.surcharges or []
1363
+ return [SharedSurchargeType.parse(surcharge) for surcharge in surcharges_data]
1364
+
1365
+ # ─────────────────────────────────────────────────────────────────
1366
+ # NEW: Service-zone rate mappings
1367
+ # ─────────────────────────────────────────────────────────────────
1368
+ @strawberry.field
1369
+ def service_rates(
1370
+ self: providers.RateSheet,
1371
+ ) -> typing.Optional[typing.List[ServiceRateType]]:
1372
+ """Service-zone rate mappings for this rate sheet."""
1373
+ rates_data = self.service_rates or []
1374
+ return [ServiceRateType.parse(rate) for rate in rates_data]
1375
+
1259
1376
  @staticmethod
1260
1377
  @utils.authentication_required
1261
1378
  def resolve(info, id: str) -> typing.Optional["RateSheetType"]:
@@ -244,6 +244,13 @@ def ensure_unique_default_related_data(
244
244
 
245
245
  @serializers.owned_model_serializer
246
246
  class ServiceLevelModelSerializer(serializers.ModelSerializer):
247
+ """
248
+ Serializer for ServiceLevel model.
249
+
250
+ Services reference shared zones and surcharges at the RateSheet level
251
+ via zone_ids and surcharge_ids.
252
+ """
253
+
247
254
  dimension_unit = serializers.CharField(
248
255
  required=False, allow_null=True, allow_blank=True
249
256
  )
@@ -257,46 +264,6 @@ class ServiceLevelModelSerializer(serializers.ModelSerializer):
257
264
  exclude = ["created_at", "updated_at", "created_by"]
258
265
  extra_kwargs = {field: {"read_only": True} for field in ["id"]}
259
266
 
260
- def update_zone(self, zone_index: int, zone_data: dict) -> None:
261
- """Update a specific zone in the service level."""
262
- if zone_index >= len(self.instance.zones):
263
- raise exceptions.ValidationError(
264
- _(f"Zone index {zone_index} is out of range"),
265
- code="invalid_zone_index",
266
- )
267
-
268
- self.instance.zones[zone_index].update(
269
- {k: v for k, v in zone_data.items() if v != strawberry.UNSET}
270
- )
271
- self.instance.save(update_fields=["zones"])
272
-
273
- def update(self, instance, validated_data, context=None, **kwargs):
274
- """Handle partial updates of service level data including zones."""
275
- zones_data = validated_data.pop("zones", None)
276
- instance = super().update(instance, validated_data, context=context)
277
-
278
- if zones_data is not None:
279
- # Handle zone updates if provided
280
- existing_zones = instance.zones or []
281
- updated_zones = []
282
-
283
- for idx, zone_data in enumerate(zones_data):
284
- if idx < len(existing_zones):
285
- # Update existing zone
286
- zone = existing_zones[idx].copy()
287
- zone.update(
288
- {k: v for k, v in zone_data.items() if v != strawberry.UNSET}
289
- )
290
- updated_zones.append(zone)
291
- else:
292
- # Add new zone
293
- updated_zones.append(zone_data)
294
-
295
- instance.zones = updated_zones
296
- instance.save(update_fields=["zones"])
297
-
298
- return instance
299
-
300
267
 
301
268
  @serializers.owned_model_serializer
302
269
  class LabelTemplateModelSerializer(serializers.ModelSerializer):
@@ -367,14 +334,110 @@ class RateSheetModelSerializer(serializers.ModelSerializer):
367
334
  carrier.rate_sheet = self.instance if carrier.id in carriers else None
368
335
  carrier.save(update_fields=["rate_sheet"])
369
336
 
337
+ def process_zones(self, zones_data: list, remove_missing: bool = False) -> None:
338
+ """Process zones for the rate sheet.
339
+
340
+ Args:
341
+ zones_data: List of zone dicts with id, label, country_codes, etc.
342
+ remove_missing: If True, remove zones not present in zones_data.
343
+ """
344
+ existing_zone_ids = {z["id"] for z in (self.instance.zones or [])}
345
+ incoming_zone_ids = set()
346
+
347
+ for zone_data in zones_data:
348
+ zone_dict = {k: v for k, v in zone_data.items() if v is not None}
349
+ zone_id = zone_dict.get("id")
350
+
351
+ if zone_id:
352
+ incoming_zone_ids.add(zone_id)
353
+
354
+ if zone_id and zone_id in existing_zone_ids:
355
+ self.instance.update_zone(zone_id, zone_dict)
356
+ else:
357
+ self.instance.add_zone(zone_dict)
358
+
359
+ # Remove zones not in incoming data
360
+ if remove_missing:
361
+ zones_to_remove = existing_zone_ids - incoming_zone_ids
362
+ for zone_id in zones_to_remove:
363
+ self.instance.remove_zone(zone_id)
364
+
365
+ def process_surcharges(self, surcharges_data: list, remove_missing: bool = False) -> None:
366
+ """Process surcharges for the rate sheet.
367
+
368
+ Args:
369
+ surcharges_data: List of surcharge dicts with id, name, amount, etc.
370
+ remove_missing: If True, remove surcharges not present in surcharges_data.
371
+ """
372
+ existing_surcharge_ids = {s["id"] for s in (self.instance.surcharges or [])}
373
+ incoming_surcharge_ids = set()
374
+
375
+ for surcharge_data in surcharges_data:
376
+ surcharge_dict = {k: v for k, v in surcharge_data.items() if v is not None}
377
+ surcharge_id = surcharge_dict.get("id")
378
+
379
+ if surcharge_id:
380
+ incoming_surcharge_ids.add(surcharge_id)
381
+
382
+ if surcharge_id and surcharge_id in existing_surcharge_ids:
383
+ self.instance.update_surcharge(surcharge_id, surcharge_dict)
384
+ else:
385
+ self.instance.add_surcharge(surcharge_dict)
386
+
387
+ # Remove surcharges not in incoming data
388
+ if remove_missing:
389
+ surcharges_to_remove = existing_surcharge_ids - incoming_surcharge_ids
390
+ for surcharge_id in surcharges_to_remove:
391
+ self.instance.remove_surcharge(surcharge_id)
392
+
393
+ def process_service_rates(
394
+ self, service_rates_data: list, temp_to_real_id_map: dict = None
395
+ ) -> None:
396
+ """Process service rates for the rate sheet.
397
+
398
+ Args:
399
+ service_rates_data: List of service rate dicts with service_id, zone_id, rate, etc.
400
+ temp_to_real_id_map: Optional mapping of temp-{idx} IDs to real service IDs.
401
+ """
402
+ temp_to_real_id_map = temp_to_real_id_map or {}
403
+
404
+ for rate_data in service_rates_data:
405
+ rate_dict = {k: v for k, v in rate_data.items() if v is not None}
406
+ service_id = rate_dict.pop("service_id", None)
407
+ zone_id = rate_dict.pop("zone_id", None)
408
+
409
+ # Map temp service ID to real ID if needed
410
+ if service_id and str(service_id).startswith("temp-"):
411
+ service_id = temp_to_real_id_map.get(service_id, service_id)
412
+
413
+ if service_id and zone_id:
414
+ self.instance.update_service_rate(service_id, zone_id, rate_dict)
415
+
416
+ def build_temp_to_real_service_map(self, services_data: list) -> dict:
417
+ """Build mapping from temp-{idx} IDs to real service IDs by service_code."""
418
+ created_services = {s.service_code: s.id for s in self.instance.services.all()}
419
+
420
+ return {
421
+ f"temp-{idx}": created_services[svc.get("service_code")]
422
+ for idx, svc in enumerate(services_data)
423
+ if svc.get("service_code") in created_services
424
+ }
425
+
370
426
  def update(self, instance, validated_data, **kwargs):
371
- """Handle updates of rate sheet data including services and carriers."""
427
+ """Handle updates of rate sheet data including services and carriers.
428
+
429
+ When zones or surcharges data is provided, it's treated as a full replacement -
430
+ zones/surcharges not in the incoming data will be removed.
431
+ """
372
432
  services_data = validated_data.pop("services", None)
373
433
  carriers = (
374
434
  validated_data.pop("carriers", None)
375
435
  if "carriers" in validated_data
376
436
  else None
377
437
  )
438
+ zones_data = validated_data.pop("zones", None)
439
+ surcharges_data = validated_data.pop("surcharges", None)
440
+ service_rates_data = validated_data.pop("service_rates", None)
378
441
  remove_missing_services = validated_data.pop("remove_missing_services", False)
379
442
 
380
443
  instance = super().update(instance, validated_data)
@@ -385,4 +448,15 @@ class RateSheetModelSerializer(serializers.ModelSerializer):
385
448
  if carriers is not None:
386
449
  self.update_carriers(carriers)
387
450
 
451
+ if zones_data:
452
+ # Full update: remove zones not present in incoming data
453
+ self.process_zones(zones_data, remove_missing=True)
454
+
455
+ if surcharges_data:
456
+ # Full update: remove surcharges not present in incoming data
457
+ self.process_surcharges(surcharges_data, remove_missing=True)
458
+
459
+ if service_rates_data:
460
+ self.process_service_rates(service_rates_data)
461
+
388
462
  return instance