Initial commit: VPN Telegram bot (sanitized defaults)
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
"""Тарифы из 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 ""),
|
||||
)
|
||||
Reference in New Issue
Block a user