Files
vpn-telegram-bot/services/tariffs.py
T

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 ""),
)