"""Тарифы из 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 ""), )