189 lines
6.0 KiB
Python
189 lines
6.0 KiB
Python
"""Тарифы из tariffs.json."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from uuid import UUID
|
|
|
|
TARIFF_ID_RE = re.compile(r"^[A-Za-z0-9_]{2,40}$")
|
|
|
|
|
|
class TariffError(ValueError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Tariff:
|
|
id: str
|
|
title: str
|
|
price: float
|
|
days: int
|
|
traffic_gb: float
|
|
device_limit: int
|
|
squad_uuids: tuple[UUID, ...]
|
|
description: str = ""
|
|
|
|
@property
|
|
def traffic_bytes(self) -> float:
|
|
# 0 или меньше → базовая квота 1000 ГБ
|
|
from services.traffic import BASE_TRAFFIC_GB, gb_to_bytes
|
|
|
|
gb = self.traffic_gb if self.traffic_gb > 0 else BASE_TRAFFIC_GB
|
|
return gb_to_bytes(gb)
|
|
|
|
def format_price(self) -> str:
|
|
if self.price == int(self.price):
|
|
return f"{int(self.price)} ₽"
|
|
return f"{self.price:.2f} ₽"
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"id": self.id,
|
|
"title": self.title,
|
|
"price": self.price if self.price != int(self.price) else int(self.price),
|
|
"days": self.days,
|
|
"traffic_gb": self.traffic_gb,
|
|
"device_limit": self.device_limit,
|
|
"squad_uuids": [str(u) for u in self.squad_uuids],
|
|
"description": self.description,
|
|
}
|
|
|
|
def to_admin_dict(self) -> dict:
|
|
d = self.to_dict()
|
|
d["price_label"] = self.format_price()
|
|
return d
|
|
|
|
|
|
def load_tariffs(path: str | Path) -> list[Tariff]:
|
|
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
tariffs: list[Tariff] = []
|
|
for item in data:
|
|
tariffs.append(_tariff_from_item(item))
|
|
return tariffs
|
|
|
|
|
|
def save_tariffs(path: str | Path, tariffs: list[Tariff]) -> None:
|
|
payload = [t.to_dict() for t in tariffs]
|
|
text = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
|
|
Path(path).write_text(text, encoding="utf-8")
|
|
|
|
|
|
def replace_shared_tariffs(shared: list[Tariff], new: list[Tariff]) -> None:
|
|
"""Обновить общий список in-place (bot + webapp + workers)."""
|
|
shared[:] = list(new)
|
|
|
|
|
|
def get_tariff(tariffs: list[Tariff], tariff_id: str) -> Tariff | None:
|
|
for t in tariffs:
|
|
if t.id == tariff_id:
|
|
return t
|
|
return None
|
|
|
|
|
|
def parse_tariff_payload(
|
|
body: dict,
|
|
*,
|
|
existing: Tariff | None = None,
|
|
default_squads: tuple[UUID, ...] = (),
|
|
) -> Tariff:
|
|
tid = str(body.get("id") or (existing.id if existing else "")).strip()
|
|
if not TARIFF_ID_RE.fullmatch(tid):
|
|
raise TariffError("ID: 2–40 символов (латиница, цифры, _)")
|
|
|
|
title = str(body.get("title") or (existing.title if existing else "")).strip()
|
|
if not title:
|
|
raise TariffError("Укажите название тарифа")
|
|
if len(title) > 80:
|
|
raise TariffError("Название слишком длинное")
|
|
|
|
try:
|
|
price = float(body.get("price") if "price" in body else (existing.price if existing else 0))
|
|
except (TypeError, ValueError) as exc:
|
|
raise TariffError("Некорректная цена") from exc
|
|
if price < 0:
|
|
raise TariffError("Цена не может быть отрицательной")
|
|
|
|
try:
|
|
days = int(body.get("days") if "days" in body else (existing.days if existing else 0))
|
|
except (TypeError, ValueError) as exc:
|
|
raise TariffError("Некорректное число дней") from exc
|
|
if days < 1 or days > 3650:
|
|
raise TariffError("Дни: от 1 до 3650")
|
|
|
|
try:
|
|
traffic_gb = float(
|
|
body.get("traffic_gb")
|
|
if "traffic_gb" in body
|
|
else (existing.traffic_gb if existing else 1000)
|
|
)
|
|
except (TypeError, ValueError) as exc:
|
|
raise TariffError("Некорректный трафик") from exc
|
|
if traffic_gb < 0:
|
|
raise TariffError("Трафик не может быть отрицательным")
|
|
|
|
try:
|
|
device_limit = int(
|
|
body.get("device_limit")
|
|
if "device_limit" in body
|
|
else (existing.device_limit if existing else 10)
|
|
)
|
|
except (TypeError, ValueError) as exc:
|
|
raise TariffError("Некорректный лимит устройств") from exc
|
|
if device_limit < 1 or device_limit > 100:
|
|
raise TariffError("Устройства: от 1 до 100")
|
|
|
|
if "squad_uuids" in body:
|
|
squads_raw = body.get("squad_uuids")
|
|
if isinstance(squads_raw, str):
|
|
parts = [p.strip() for p in squads_raw.replace(";", ",").split(",") if p.strip()]
|
|
elif isinstance(squads_raw, list):
|
|
parts = [str(x).strip() for x in squads_raw if str(x).strip()]
|
|
else:
|
|
parts = []
|
|
try:
|
|
squad_uuids = tuple(UUID(p) for p in parts)
|
|
except ValueError as exc:
|
|
raise TariffError("Некорректный UUID в squad_uuids") from exc
|
|
elif existing:
|
|
squad_uuids = existing.squad_uuids
|
|
else:
|
|
squad_uuids = default_squads
|
|
|
|
if not squad_uuids:
|
|
raise TariffError("Нужен хотя бы один squad UUID")
|
|
|
|
description = str(
|
|
body.get("description")
|
|
if "description" in body
|
|
else (existing.description if existing else "")
|
|
).strip()
|
|
if len(description) > 300:
|
|
raise TariffError("Описание слишком длинное")
|
|
|
|
return Tariff(
|
|
id=tid,
|
|
title=title,
|
|
price=price,
|
|
days=days,
|
|
traffic_gb=traffic_gb,
|
|
device_limit=device_limit,
|
|
squad_uuids=squad_uuids,
|
|
description=description,
|
|
)
|
|
|
|
|
|
def _tariff_from_item(item: dict) -> Tariff:
|
|
return Tariff(
|
|
id=str(item["id"]),
|
|
title=str(item["title"]),
|
|
price=float(item["price"]),
|
|
days=int(item["days"]),
|
|
traffic_gb=float(item.get("traffic_gb") or 0),
|
|
device_limit=int(item.get("device_limit") or 1),
|
|
squad_uuids=tuple(UUID(str(u)) for u in item.get("squad_uuids") or []),
|
|
description=str(item.get("description") or ""),
|
|
)
|