diff --git a/tgbot/database/__init__.py b/tgbot/database/__init__.py
index da6dde5..d2c3186 100644
--- a/tgbot/database/__init__.py
+++ b/tgbot/database/__init__.py
@@ -6,7 +6,29 @@ from .db_purchases import PurchaseModel, Purchasesx
from .db_refill import RefillModel, Refillx
from .db_settings import SettingsModel, Settingsx
from .db_users import UserModel, UsersRepository, Userx
-from .entities import Category, Item, Payments, Position, Purchase, Refill, Settings, User
+
+from .db_referral import (
+ ReferralModel,
+ ReferralTransactionModel,
+ ReferralWithdrawalModel,
+ Referralx,
+ ReferralTransactionx,
+ ReferralWithdrawalx,
+)
+
+from .entities import (
+ Category,
+ Item,
+ Payments,
+ Position,
+ Purchase,
+ Refill,
+ Settings,
+ User,
+ Referral,
+ ReferralTransaction,
+ ReferralWithdrawal,
+)
ModelCategory = Category
ModelItem = Item
@@ -18,3 +40,7 @@ ModelSettings = Settings
ModelUser = User
ModelUsers = User
SettingsRepository = Settingsx
+
+ModelReferral = Referral
+ModelReferralTransaction = ReferralTransaction
+ModelReferralWithdrawal = ReferralWithdrawal
diff --git a/tgbot/database/db_referral.py b/tgbot/database/db_referral.py
new file mode 100644
index 0000000..5751861
--- /dev/null
+++ b/tgbot/database/db_referral.py
@@ -0,0 +1,728 @@
+# - *- coding: utf- 8 - *-
+from math import isfinite
+import re
+from urllib.parse import urlparse
+from uuid import uuid4
+
+from sqlalchemy import BigInteger, Float, Integer, String, UniqueConstraint, text
+from sqlalchemy.orm import Mapped, mapped_column
+from sqlalchemy.ext.asyncio import AsyncConnection
+
+from typing import Any, Dict, Optional, Tuple, Union
+
+from tgbot.database.core import Base, session_factory
+from tgbot.database.entities import Referral, ReferralTransaction, ReferralWithdrawal
+from tgbot.database.repository import BaseRepository
+from tgbot.utils.const_functions import get_unix
+
+
+def is_valid_referral_withdrawal_recipient(
+ withdrawal_method: str, recipient: str
+) -> bool:
+ if withdrawal_method == "Cryptobot":
+ return bool(re.fullmatch(r"@[A-Za-z][A-Za-z0-9_]{4,31}", recipient))
+
+ if withdrawal_method == "Lolzteam":
+ parsed_url = urlparse(recipient)
+ return parsed_url.scheme in ("http", "https") and parsed_url.hostname in (
+ "zelenka.guru",
+ "lolz.team",
+ "lolz.live",
+ "lolz.guru",
+ )
+
+ return False
+
+
+async def ensure_referrals_schema(conn: AsyncConnection) -> None:
+ user_columns_result = await conn.exec_driver_sql("PRAGMA table_info(storage_users)")
+ user_columns = {column[1] for column in user_columns_result.fetchall()}
+
+ # Обновление полей пользователя
+ if user_columns:
+ if "user_referrer_id" not in user_columns:
+ await conn.exec_driver_sql(
+ "ALTER TABLE storage_users ADD COLUMN user_referrer_id BIGINT"
+ )
+ if "user_referral_balance" not in user_columns:
+ await conn.exec_driver_sql(
+ "ALTER TABLE storage_users "
+ "ADD COLUMN user_referral_balance FLOAT NOT NULL DEFAULT 0"
+ )
+ if "user_referral_hold" not in user_columns:
+ await conn.exec_driver_sql(
+ "ALTER TABLE storage_users "
+ "ADD COLUMN user_referral_hold FLOAT NOT NULL DEFAULT 0"
+ )
+
+ await conn.exec_driver_sql(
+ "CREATE INDEX IF NOT EXISTS ix_storage_users_user_referrer_id "
+ "ON storage_users (user_referrer_id)"
+ )
+
+ settings_columns_result = await conn.exec_driver_sql(
+ "PRAGMA table_info(storage_settings)"
+ )
+ settings_columns = {column[1] for column in settings_columns_result.fetchall()}
+
+ # Обновление полей настроек
+ if settings_columns:
+ if "status_referral" not in settings_columns:
+ await conn.exec_driver_sql(
+ "ALTER TABLE storage_settings "
+ "ADD COLUMN status_referral VARCHAR(16) NOT NULL DEFAULT 'False'"
+ )
+ if "referral_bonus_rub" not in settings_columns:
+ await conn.exec_driver_sql(
+ "ALTER TABLE storage_settings "
+ "ADD COLUMN referral_bonus_rub FLOAT NOT NULL DEFAULT 0"
+ )
+ if "referral_refill_percent" not in settings_columns:
+ await conn.exec_driver_sql(
+ "ALTER TABLE storage_settings "
+ "ADD COLUMN referral_refill_percent FLOAT NOT NULL DEFAULT 0"
+ )
+
+
+class ReferralModel(Base):
+ __tablename__ = "storage_referrals"
+
+ increment: Mapped[int] = mapped_column(
+ Integer, primary_key=True, autoincrement=True
+ )
+ referrer_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
+ referral_id: Mapped[int] = mapped_column(
+ BigInteger, nullable=False, unique=True, index=True
+ )
+ referral_unix: Mapped[int] = mapped_column(
+ Integer, nullable=False, default=get_unix
+ )
+
+
+class ReferralTransactionModel(Base):
+ __tablename__ = "storage_referral_transactions"
+ __table_args__ = (
+ UniqueConstraint(
+ "user_id",
+ "transaction_type",
+ "source_id",
+ name="uq_referral_transaction_source",
+ ),
+ )
+
+ increment: Mapped[int] = mapped_column(
+ Integer, primary_key=True, autoincrement=True
+ )
+ user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
+ related_user_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)
+
+ transaction_type: Mapped[str] = mapped_column(String(64), nullable=False)
+ source_id: Mapped[str] = mapped_column(String(128), nullable=False)
+ amount: Mapped[float] = mapped_column(Float, nullable=False, default=0)
+ transaction_unix: Mapped[int] = mapped_column(
+ Integer, nullable=False, default=get_unix
+ )
+
+
+class ReferralWithdrawalModel(Base):
+ __tablename__ = "storage_referral_withdrawals"
+
+ increment: Mapped[int] = mapped_column(
+ Integer, primary_key=True, autoincrement=True
+ )
+ user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True)
+ withdrawal_amount: Mapped[float] = mapped_column(Float, nullable=False, default=0)
+ withdrawal_method: Mapped[str] = mapped_column(String(32), nullable=False)
+ withdrawal_recipient: Mapped[str] = mapped_column(String(255), nullable=False)
+ withdrawal_status: Mapped[str] = mapped_column(
+ String(32), nullable=False, default="pending"
+ )
+ withdrawal_unix: Mapped[int] = mapped_column(
+ Integer, nullable=False, default=get_unix
+ )
+
+ processed_unix: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
+ processed_admin_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)
+
+
+class Referralx(BaseRepository[ReferralModel, Referral]):
+ def __init__(self):
+ super().__init__()
+ self.table_model = ReferralModel
+ self.entity_model = Referral
+ self.storage_name = ReferralModel.__tablename__
+
+ async def add(self, referrer_id: int, referral_id: int) -> Referral:
+ return await self._insert(
+ referrer_id=referrer_id,
+ referral_id=referral_id,
+ referral_unix=get_unix(),
+ )
+
+ async def register_referral(self, referral_id: int, referrer_id: int) -> str:
+ if referral_id == referrer_id:
+ return "SELF_REFERRAL"
+
+ async with session_factory() as session:
+ try:
+ await session.execute(text("BEGIN IMMEDIATE"))
+
+ settings_result = await session.execute(
+ text(
+ "SELECT status_referral, referral_bonus_rub "
+ "FROM storage_settings WHERE id = 1"
+ )
+ )
+ settings = settings_result.mappings().first()
+
+ if settings is None or settings["status_referral"] != "True":
+ await session.rollback()
+ return "DISABLED"
+
+ referrer_result = await session.execute(
+ text(
+ "SELECT user_referral_balance FROM storage_users "
+ "WHERE user_id = :user_id"
+ ),
+ {"user_id": referrer_id},
+ )
+ referrer = referrer_result.mappings().first()
+
+ if referrer is None:
+ await session.rollback()
+ return "REFERRER_NOT_FOUND"
+
+ referral_result = await session.execute(
+ text(
+ "SELECT user_balance, user_referrer_id "
+ "FROM storage_users WHERE user_id = :user_id"
+ ),
+ {"user_id": referral_id},
+ )
+ referral = referral_result.mappings().first()
+
+ if referral is None:
+ await session.rollback()
+ return "REFERRAL_NOT_FOUND"
+
+ existing_referral_result = await session.execute(
+ text(
+ "SELECT increment FROM storage_referrals "
+ "WHERE referral_id = :user_id LIMIT 1"
+ ),
+ {"user_id": referral_id},
+ )
+
+ if (
+ referral["user_referrer_id"] is not None
+ or existing_referral_result.mappings().first() is not None
+ ):
+ await session.rollback()
+ return "ALREADY"
+
+ bonus = round(float(settings["referral_bonus_rub"]), 2)
+ source_id = f"registration:{referral_id}"
+ now_unix = get_unix()
+
+ # Приглашённому — бонус на основной баланс.
+ await session.execute(
+ text(
+ "INSERT INTO storage_referrals "
+ "(referrer_id, referral_id, referral_unix) "
+ "VALUES (:referrer_id, :referral_id, :referral_unix)"
+ ),
+ {
+ "referrer_id": referrer_id,
+ "referral_id": referral_id,
+ "referral_unix": now_unix,
+ },
+ )
+ await session.execute(
+ text(
+ "UPDATE storage_users "
+ "SET user_referrer_id = :referrer_id, "
+ "user_balance = :balance "
+ "WHERE user_id = :user_id"
+ ),
+ {
+ "referrer_id": referrer_id,
+ "balance": round(float(referral["user_balance"]) + bonus, 2),
+ "user_id": referral_id,
+ },
+ )
+
+ # Рефереру — бонус на реферальный баланс.
+ await session.execute(
+ text(
+ "UPDATE storage_users "
+ "SET user_referral_balance = :balance "
+ "WHERE user_id = :user_id"
+ ),
+ {
+ "balance": round(
+ float(referrer["user_referral_balance"]) + bonus, 2
+ ),
+ "user_id": referrer_id,
+ },
+ )
+ await session.execute(
+ text(
+ "INSERT INTO storage_referral_transactions "
+ "(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
+ "VALUES (:user_id, :related_user_id, :transaction_type, :source_id, :amount, :transaction_unix)"
+ ),
+ {
+ "user_id": referral_id,
+ "related_user_id": referrer_id,
+ "transaction_type": "registration_bonus_main",
+ "source_id": source_id,
+ "amount": bonus,
+ "transaction_unix": now_unix,
+ },
+ )
+ await session.execute(
+ text(
+ "INSERT INTO storage_referral_transactions "
+ "(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
+ "VALUES (:user_id, :related_user_id, :transaction_type, :source_id, :amount, :transaction_unix)"
+ ),
+ {
+ "user_id": referrer_id,
+ "related_user_id": referral_id,
+ "transaction_type": "registration_bonus_referral",
+ "source_id": source_id,
+ "amount": bonus,
+ "transaction_unix": now_unix,
+ },
+ )
+ await session.commit()
+
+ except Exception:
+ await session.rollback()
+ raise
+
+ return "ok"
+
+ async def transfer_to_main_balance(self, user_id: int, amount: float) -> str:
+ try:
+ transfer_amount = round(float(amount), 2)
+ except (TypeError, ValueError):
+ return "INVALID_AMOUNT"
+
+ if not isfinite(transfer_amount) or transfer_amount <= 0:
+ return "INVALID_AMOUNT"
+
+ async with session_factory() as session:
+ try:
+ await session.execute(text("BEGIN IMMEDIATE"))
+
+ user_result = await session.execute(
+ text(
+ "SELECT user_balance, user_referral_balance, user_referral_hold "
+ "FROM storage_users WHERE user_id = :user_id"
+ ),
+ {"user_id": user_id},
+ )
+ get_user = user_result.mappings().first()
+
+ if get_user is None:
+ await session.rollback()
+ return "USER_NOT_FOUND"
+
+ available_balance = round(
+ float(get_user["user_referral_balance"])
+ - float(get_user["user_referral_hold"]),
+ 2,
+ )
+
+ if transfer_amount > available_balance:
+ await session.rollback()
+ return "INSUFFICIENT_FUNDS"
+
+ await session.execute(
+ text(
+ "UPDATE storage_users "
+ "SET user_balance = :user_balance, "
+ "user_referral_balance = :referral_balance "
+ "WHERE user_id = :user_id"
+ ),
+ {
+ "user_balance": round(
+ float(get_user["user_balance"]) + transfer_amount,
+ 2,
+ ),
+ "referral_balance": round(
+ float(get_user["user_referral_balance"]) - transfer_amount,
+ 2,
+ ),
+ "user_id": user_id,
+ },
+ )
+ await session.execute(
+ text(
+ "INSERT INTO storage_referral_transactions "
+ "(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
+ "VALUES (:user_id, NULL, :transaction_type, :source_id, :amount, :transaction_unix)"
+ ),
+ {
+ "user_id": user_id,
+ "transaction_type": "transfer_to_main",
+ "source_id": f"transfer:{user_id}:{uuid4().hex}",
+ "amount": transfer_amount,
+ "transaction_unix": get_unix(),
+ },
+ )
+ await session.commit()
+ except Exception:
+ await session.rollback()
+ raise
+
+ return "ok"
+
+
+class ReferralTransactionx(
+ BaseRepository[ReferralTransactionModel, ReferralTransaction]
+):
+ def __init__(self):
+ super().__init__()
+ self.table_model = ReferralTransactionModel
+ self.entity_model = ReferralTransaction
+ self.storage_name = ReferralTransactionModel.__tablename__
+
+ async def get_refill_reward(self, source_id: str) -> Optional[ReferralTransaction]:
+ return await self.get(
+ transaction_type="refill_percent",
+ source_id=source_id,
+ )
+
+ async def add(
+ self,
+ user_id: int,
+ transaction_type: str,
+ source_id: str,
+ amount: float,
+ related_user_id: Optional[int] = None,
+ ) -> ReferralTransaction:
+ return await self._insert(
+ user_id=user_id,
+ related_user_id=related_user_id,
+ transaction_type=transaction_type,
+ source_id=source_id,
+ amount=round(amount, 2),
+ transaction_unix=get_unix(),
+ )
+
+
+class ReferralWithdrawalx(BaseRepository[ReferralWithdrawalModel, ReferralWithdrawal]):
+ def __init__(self):
+ super().__init__()
+ self.table_model = ReferralWithdrawalModel
+ self.entity_model = ReferralWithdrawal
+ self.storage_name = ReferralWithdrawalModel.__tablename__
+
+ async def create_pending(
+ self,
+ user_id: int,
+ withdrawal_amount: float,
+ withdrawal_method: str,
+ withdrawal_recipient: str,
+ ) -> Tuple[str, Optional[ReferralWithdrawal]]:
+ try:
+ amount = round(float(withdrawal_amount), 2)
+ except (TypeError, ValueError):
+ return "INVALID_AMOUNT", None
+
+ recipient = str(withdrawal_recipient).strip()
+
+ if not isfinite(amount) or amount <= 0:
+ return "INVALID_AMOUNT", None
+
+ if withdrawal_method not in ("Cryptobot", "Lolzteam"):
+ return "INVALID_METHOD", None
+
+ if not is_valid_referral_withdrawal_recipient(withdrawal_method, recipient):
+ return "INVALID_RECIPIENT", None
+
+ withdrawal_id: Optional[int] = None
+
+ async with session_factory() as session:
+ try:
+ await session.execute(text("BEGIN IMMEDIATE"))
+
+ user_result = await session.execute(
+ text(
+ "SELECT user_referral_balance, user_referral_hold "
+ "FROM storage_users WHERE user_id = :user_id"
+ ),
+ {"user_id": user_id},
+ )
+ get_user = user_result.mappings().first()
+
+ if get_user is None:
+ await session.rollback()
+ return "USER_NOT_FOUND", None
+
+ available_balance = round(
+ float(get_user["user_referral_balance"])
+ - float(get_user["user_referral_hold"]),
+ 2,
+ )
+
+ if amount > available_balance:
+ await session.rollback()
+ return "INSUFFICIENT_FUNDS", None
+
+ now_unix = get_unix()
+ withdrawal_result = await session.execute(
+ text(
+ "INSERT INTO storage_referral_withdrawals "
+ "(user_id, withdrawal_amount, withdrawal_method, withdrawal_recipient, withdrawal_status, withdrawal_unix) "
+ "VALUES (:user_id, :amount, :method, :recipient, 'pending', :unix)"
+ ),
+ {
+ "user_id": user_id,
+ "amount": amount,
+ "method": withdrawal_method,
+ "recipient": recipient,
+ "unix": now_unix,
+ },
+ )
+ withdrawal_id = int(withdrawal_result.lastrowid)
+
+ await session.execute(
+ text(
+ "UPDATE storage_users "
+ "SET user_referral_hold = :hold "
+ "WHERE user_id = :user_id"
+ ),
+ {
+ "hold": round(
+ float(get_user["user_referral_hold"]) + amount,
+ 2,
+ ),
+ "user_id": user_id,
+ },
+ )
+ await session.execute(
+ text(
+ "INSERT INTO storage_referral_transactions "
+ "(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
+ "VALUES (:user_id, NULL, :transaction_type, :source_id, :amount, :unix)"
+ ),
+ {
+ "user_id": user_id,
+ "transaction_type": "withdrawal_hold",
+ "source_id": f"withdrawal:{withdrawal_id}",
+ "amount": amount,
+ "unix": now_unix,
+ },
+ )
+ await session.commit()
+ except Exception:
+ await session.rollback()
+ raise
+
+ return "ok", await self.get(increment=withdrawal_id)
+
+ async def complete(
+ self, withdrawal_id: int, admin_id: int
+ ) -> Tuple[str, Optional[ReferralWithdrawal]]:
+ async with session_factory() as session:
+ try:
+ await session.execute(text("BEGIN IMMEDIATE"))
+
+ withdrawal_result = await session.execute(
+ text(
+ "SELECT user_id, withdrawal_amount, withdrawal_status "
+ "FROM storage_referral_withdrawals WHERE increment = :withdrawal_id"
+ ),
+ {"withdrawal_id": withdrawal_id},
+ )
+ withdrawal = withdrawal_result.mappings().first()
+
+ if withdrawal is None:
+ await session.rollback()
+ return "NOT_FOUND", None
+ if withdrawal["withdrawal_status"] != "pending":
+ await session.rollback()
+ return "ALREADY_PROCESSED", None
+
+ user_result = await session.execute(
+ text(
+ "SELECT user_referral_balance, user_referral_hold "
+ "FROM storage_users WHERE user_id = :user_id"
+ ),
+ {"user_id": withdrawal["user_id"]},
+ )
+ get_user = user_result.mappings().first()
+ amount = float(withdrawal["withdrawal_amount"])
+
+ if (
+ get_user is None
+ or float(get_user["user_referral_balance"]) < amount
+ or float(get_user["user_referral_hold"]) < amount
+ ):
+ await session.rollback()
+ return "BALANCE_ERROR", None
+
+ now_unix = get_unix()
+ await session.execute(
+ text(
+ "UPDATE storage_users "
+ "SET user_referral_balance = :balance, user_referral_hold = :hold "
+ "WHERE user_id = :user_id"
+ ),
+ {
+ "balance": round(
+ float(get_user["user_referral_balance"]) - amount,
+ 2,
+ ),
+ "hold": round(
+ float(get_user["user_referral_hold"]) - amount,
+ 2,
+ ),
+ "user_id": withdrawal["user_id"],
+ },
+ )
+ await session.execute(
+ text(
+ "UPDATE storage_referral_withdrawals "
+ "SET withdrawal_status = 'completed', processed_unix = :unix, processed_admin_id = :admin_id "
+ "WHERE increment = :withdrawal_id"
+ ),
+ {
+ "unix": now_unix,
+ "admin_id": admin_id,
+ "withdrawal_id": withdrawal_id,
+ },
+ )
+ await session.execute(
+ text(
+ "INSERT INTO storage_referral_transactions "
+ "(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
+ "VALUES (:user_id, NULL, :transaction_type, :source_id, :amount, :unix)"
+ ),
+ {
+ "user_id": withdrawal["user_id"],
+ "transaction_type": "withdrawal_completed",
+ "source_id": f"withdrawal:{withdrawal_id}",
+ "amount": amount,
+ "unix": now_unix,
+ },
+ )
+ await session.commit()
+ except Exception:
+ await session.rollback()
+ raise
+
+ return "ok", await self.get(increment=withdrawal_id)
+
+ async def reject(
+ self, withdrawal_id: int, admin_id: int
+ ) -> Tuple[str, Optional[ReferralWithdrawal]]:
+ async with session_factory() as session:
+ try:
+ await session.execute(text("BEGIN IMMEDIATE"))
+
+ withdrawal_result = await session.execute(
+ text(
+ "SELECT user_id, withdrawal_amount, withdrawal_status "
+ "FROM storage_referral_withdrawals WHERE increment = :withdrawal_id"
+ ),
+ {"withdrawal_id": withdrawal_id},
+ )
+ withdrawal = withdrawal_result.mappings().first()
+
+ if withdrawal is None:
+ await session.rollback()
+ return "NOT_FOUND", None
+ if withdrawal["withdrawal_status"] != "pending":
+ await session.rollback()
+ return "ALREADY_PROCESSED", None
+
+ user_result = await session.execute(
+ text(
+ "SELECT user_referral_hold FROM storage_users "
+ "WHERE user_id = :user_id"
+ ),
+ {"user_id": withdrawal["user_id"]},
+ )
+ get_user = user_result.mappings().first()
+ amount = float(withdrawal["withdrawal_amount"])
+
+ if get_user is None or float(get_user["user_referral_hold"]) < amount:
+ await session.rollback()
+ return "BALANCE_ERROR", None
+
+ now_unix = get_unix()
+ await session.execute(
+ text(
+ "UPDATE storage_users "
+ "SET user_referral_hold = :hold WHERE user_id = :user_id"
+ ),
+ {
+ "hold": round(
+ float(get_user["user_referral_hold"]) - amount,
+ 2,
+ ),
+ "user_id": withdrawal["user_id"],
+ },
+ )
+ await session.execute(
+ text(
+ "UPDATE storage_referral_withdrawals "
+ "SET withdrawal_status = 'rejected', processed_unix = :unix, processed_admin_id = :admin_id "
+ "WHERE increment = :withdrawal_id"
+ ),
+ {
+ "unix": now_unix,
+ "admin_id": admin_id,
+ "withdrawal_id": withdrawal_id,
+ },
+ )
+ await session.execute(
+ text(
+ "INSERT INTO storage_referral_transactions "
+ "(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
+ "VALUES (:user_id, NULL, :transaction_type, :source_id, :amount, :unix)"
+ ),
+ {
+ "user_id": withdrawal["user_id"],
+ "transaction_type": "withdrawal_rejected",
+ "source_id": f"withdrawal:{withdrawal_id}",
+ "amount": amount,
+ "unix": now_unix,
+ },
+ )
+ await session.commit()
+ except Exception:
+ await session.rollback()
+ raise
+
+ return "ok", await self.get(increment=withdrawal_id)
+
+ async def add(
+ self,
+ user_id: int,
+ withdrawal_amount: float,
+ withdrawal_method: str,
+ withdrawal_recipient: str,
+ ) -> ReferralWithdrawal:
+ return await self._insert(
+ user_id=user_id,
+ withdrawal_amount=round(withdrawal_amount, 2),
+ withdrawal_method=withdrawal_method,
+ withdrawal_recipient=withdrawal_recipient,
+ withdrawal_status="pending",
+ withdrawal_unix=get_unix(),
+ )
+
+ async def update(
+ self,
+ where: Optional[Union[Dict[str, Any], int]] = None,
+ **kwargs,
+ ) -> int:
+ if isinstance(where, int):
+ where = {"increment": where}
+
+ return await self._update(where=where, **kwargs)
diff --git a/tgbot/database/db_refill.py b/tgbot/database/db_refill.py
index d2159e8..cab6209 100644
--- a/tgbot/database/db_refill.py
+++ b/tgbot/database/db_refill.py
@@ -90,7 +90,7 @@ class Refillx(BaseRepository[RefillModel, Refill]):
user_result = await session.execute(
text(
"""
- SELECT user_balance, user_refill
+ SELECT user_balance, user_refill, user_referrer_id
FROM storage_users
WHERE user_id = :user_id
"""
@@ -103,6 +103,14 @@ class Refillx(BaseRepository[RefillModel, Refill]):
await session.rollback()
return "USER_NOT_FOUND"
+ settings_result = await session.execute(
+ text(
+ "SELECT status_referral, referral_refill_percent "
+ "FROM storage_settings WHERE id = 1"
+ )
+ )
+ get_settings = settings_result.mappings().first()
+
new_balance = round(float(get_user["user_balance"]) + float(pay_amount), 2)
new_refill = round(float(get_user["user_refill"]) + float(pay_amount), 2)
@@ -138,6 +146,66 @@ class Refillx(BaseRepository[RefillModel, Refill]):
),
{"balance": new_balance, "refill": new_refill, "user_id": user_id},
)
+
+ referrer_id = get_user["user_referrer_id"]
+ referral_reward = 0.0
+
+ if (
+ get_settings is not None
+ and get_settings["status_referral"] == "True"
+ and referrer_id is not None
+ and referrer_id != user_id
+ ):
+ referral_reward = round(
+ float(pay_amount)
+ * float(get_settings["referral_refill_percent"])
+ / 100,
+ 2,
+ )
+
+ if referral_reward > 0:
+ referrer_result = await session.execute(
+ text(
+ "SELECT user_referral_balance FROM storage_users "
+ "WHERE user_id = :user_id"
+ ),
+ {"user_id": referrer_id},
+ )
+ get_referrer = referrer_result.mappings().first()
+
+ if get_referrer is not None:
+ source_id = f"refill:{pay_comment or pay_receipt}"
+
+ await session.execute(
+ text(
+ "UPDATE storage_users "
+ "SET user_referral_balance = :balance "
+ "WHERE user_id = :user_id"
+ ),
+ {
+ "balance": round(
+ float(get_referrer["user_referral_balance"])
+ + referral_reward,
+ 2,
+ ),
+ "user_id": referrer_id,
+ },
+ )
+ await session.execute(
+ text(
+ "INSERT INTO storage_referral_transactions "
+ "(user_id, related_user_id, transaction_type, source_id, amount, transaction_unix) "
+ "VALUES (:user_id, :related_user_id, :transaction_type, :source_id, :amount, :transaction_unix)"
+ ),
+ {
+ "user_id": referrer_id,
+ "related_user_id": user_id,
+ "transaction_type": "refill_percent",
+ "source_id": source_id,
+ "amount": referral_reward,
+ "transaction_unix": get_unix(),
+ },
+ )
await session.commit()
except Exception:
await session.rollback()
diff --git a/tgbot/database/db_settings.py b/tgbot/database/db_settings.py
index 3ac9810..f219c43 100644
--- a/tgbot/database/db_settings.py
+++ b/tgbot/database/db_settings.py
@@ -1,7 +1,7 @@
# - *- coding: utf- 8 - *-
from typing import Any, Dict, Optional
-from sqlalchemy import Integer, String
+from sqlalchemy import Integer, String, Float
from sqlalchemy.orm import Mapped, mapped_column
from tgbot.database.core import Base
@@ -46,6 +46,15 @@ class SettingsModel(Base):
misc_profit_week: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
misc_profit_month: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
+ # Реферальная система
+ referral_refill_percent: Mapped[float] = mapped_column(
+ Float, nullable=False, default=0
+ )
+ status_referral: Mapped[str] = mapped_column(
+ String(16), nullable=False, default="False"
+ )
+ referral_bonus_rub: Mapped[float] = mapped_column(Float, nullable=False, default=0)
+
ModelBase = Settings
BaseModel = Settings
diff --git a/tgbot/database/db_users.py b/tgbot/database/db_users.py
index 30b9c13..3eedf65 100644
--- a/tgbot/database/db_users.py
+++ b/tgbot/database/db_users.py
@@ -15,8 +15,12 @@ from tgbot.utils.const_functions import get_unix
class UserModel(Base):
__tablename__ = "storage_users"
- increment: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
- user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, unique=True, index=True)
+ increment: Mapped[int] = mapped_column(
+ Integer, primary_key=True, autoincrement=True
+ )
+ user_id: Mapped[int] = mapped_column(
+ BigInteger, nullable=False, unique=True, index=True
+ )
user_login: Mapped[str] = mapped_column(String(255), nullable=False, default="")
user_name: Mapped[str] = mapped_column(String(255), nullable=False, default="")
user_surname: Mapped[str] = mapped_column(String(255), nullable=False, default="")
@@ -26,6 +30,13 @@ class UserModel(Base):
user_give: Mapped[float] = mapped_column(Float, nullable=False, default=0)
user_unix: Mapped[int] = mapped_column(Integer, nullable=False, default=get_unix)
+ # Поля для реферальной системы
+ user_referrer_id: Mapped[int] = mapped_column(BigInteger, nullable=True, index=True)
+ user_referral_balance: Mapped[float] = mapped_column(
+ Float, nullable=False, default=0
+ )
+ user_referral_hold: Mapped[float] = mapped_column(Float, nullable=False, default=0)
+
ModelBase = User
BaseModel = User
@@ -41,31 +52,34 @@ class UsersRepository(BaseRepository[UserModel, User]):
# Добавление или обновление пользователя
async def add(
- self,
- user_id: int,
- user_login: str,
- user_name: str,
- user_surname: str = "",
- user_fullname: str = "",
+ self,
+ user_id: int,
+ user_login: str,
+ user_name: str,
+ user_surname: str = "",
+ user_fullname: str = "",
) -> User:
return await self.upsert(
user_id=user_id,
user_login=user_login,
user_name=user_name,
user_surname=user_surname,
- user_fullname=user_fullname or " ".join(filter(None, [user_name, user_surname])),
+ user_fullname=user_fullname
+ or " ".join(filter(None, [user_name, user_surname])),
)
# Выполнение upsert пользователя по телеграм ID
async def upsert(
- self,
- user_id: int,
- user_login: str,
- user_name: str,
- user_surname: str = "",
- user_fullname: str = "",
+ self,
+ user_id: int,
+ user_login: str,
+ user_name: str,
+ user_surname: str = "",
+ user_fullname: str = "",
) -> User:
- user_fullname = user_fullname or " ".join(filter(None, [user_name, user_surname]))
+ user_fullname = user_fullname or " ".join(
+ filter(None, [user_name, user_surname])
+ )
user_table = UserModel.__table__
statement = insert(UserModel).values(
user_id=user_id,
@@ -105,7 +119,9 @@ class UsersRepository(BaseRepository[UserModel, User]):
return user
# Обновление пользователя по ID или фильтру
- async def update(self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs) -> int:
+ async def update(
+ self, where: Optional[Union[Dict[str, Any], int]] = None, **kwargs
+ ) -> int:
if isinstance(where, int):
where = {"user_id": where}
diff --git a/tgbot/database/entities.py b/tgbot/database/entities.py
index a022b7f..3038d34 100644
--- a/tgbot/database/entities.py
+++ b/tgbot/database/entities.py
@@ -97,6 +97,11 @@ class Settings:
misc_profit_week: int
misc_profit_month: int
+ # Реферальная система
+ referral_refill_percent: float
+ status_referral: str
+ referral_bonus_rub: float
+
@dataclass
class User:
@@ -110,3 +115,40 @@ class User:
user_refill: float
user_give: float
user_unix: int
+
+ # Поля для реферальной системы
+ user_referrer_id: Optional[int]
+ user_referral_balance: float
+ user_referral_hold: float
+
+
+@dataclass
+class Referral:
+ increment: int
+ referrer_id: int
+ referral_id: int
+ referral_unix: int
+
+
+@dataclass
+class ReferralTransaction:
+ increment: int
+ user_id: int
+ related_user_id: Optional[int]
+ transaction_type: str
+ source_id: str
+ amount: float
+ transaction_unix: int
+
+
+@dataclass
+class ReferralWithdrawal:
+ increment: int
+ user_id: int
+ withdrawal_amount: float
+ withdrawal_method: str
+ withdrawal_recipient: str
+ withdrawal_status: str
+ withdrawal_unix: int
+ processed_unix: Optional[int]
+ processed_admin_id: Optional[int]
diff --git a/tgbot/database/repository.py b/tgbot/database/repository.py
index dddf6db..70a39ef 100644
--- a/tgbot/database/repository.py
+++ b/tgbot/database/repository.py
@@ -160,10 +160,13 @@ async def prepare_database() -> None:
import tgbot.database.db_refill # noqa: F401
import tgbot.database.db_settings # noqa: F401
import tgbot.database.db_users # noqa: F401
+ import tgbot.database.db_referral # noqa: F401
async with engine.begin() as conn:
from tgbot.database.db_payments import ensure_payments_schema
+ from tgbot.database.db_referral import ensure_referrals_schema
+ await ensure_referrals_schema(conn)
await ensure_payments_schema(conn)
await conn.run_sync(Base.metadata.create_all)
diff --git a/tgbot/keyboards/inline_admin.py b/tgbot/keyboards/inline_admin.py
index 7fe60b5..ba5d47d 100644
--- a/tgbot/keyboards/inline_admin.py
+++ b/tgbot/keyboards/inline_admin.py
@@ -20,6 +20,17 @@ def close_finl() -> InlineKeyboardMarkup:
# Рассылка
+def referral_withdrawal_actions_finl(withdrawal_id: int) -> InlineKeyboardMarkup:
+ keyboard = InlineKeyboardBuilder()
+
+ keyboard.row(
+ ikb("✅ Выполнено", data=f"referral_withdrawal_complete:{withdrawal_id}"),
+ ikb("❌ Отклонить", data=f"referral_withdrawal_reject:{withdrawal_id}"),
+ )
+
+ return keyboard.as_markup()
+
+
def mail_confirm_finl() -> InlineKeyboardMarkup:
keyboard = InlineKeyboardBuilder()
@@ -207,6 +218,17 @@ async def settings_finl() -> InlineKeyboardMarkup:
else:
method_prod_kb = ikb("На каждой строке", data="settings_edit_method_prod:skip")
+ # Бонус за приглашение
+ referral_bonus_kb = ikb(
+ f"{get_settings.referral_bonus_rub}₽", data="settings_edit_referral_bonus"
+ )
+
+ # Доход с пополнений
+ deposit_percent_kb = ikb(
+ f"{get_settings.referral_refill_percent}%",
+ data="settings_edit_referral_percent",
+ )
+
keyboard.row(
ikb("❔ FAQ", data="..."),
faq_kb,
@@ -222,6 +244,12 @@ async def settings_finl() -> InlineKeyboardMarkup:
).row(
ikb("🎁 Метод добавления", data="..."),
method_prod_kb,
+ ).row(
+ ikb("🤝 Бонус за приглашение", data="..."),
+ referral_bonus_kb,
+ ).row(
+ ikb("📈 Доход с пополнений", data="..."),
+ deposit_percent_kb,
)
return keyboard.as_markup()
@@ -236,6 +264,7 @@ async def settings_status_finl() -> InlineKeyboardMarkup:
status_work_kb = ikb("Включены ✅", data="settings_status_work:False")
status_buy_kb = ikb("Включены ✅", data="settings_status_buy:False")
status_refill_kb = ikb("Включены ✅", data="settings_status_refill:False")
+ status_referral_kb = ikb("Включена ✅", data="settings_status_referral:False")
notification_buy_kb = ikb("Включены 🔔", data="settings_notification_buy:False")
notification_refill_kb = ikb(
"Включены 🔔", data="settings_notification_refill:False"
@@ -247,6 +276,8 @@ async def settings_status_finl() -> InlineKeyboardMarkup:
status_work_kb = ikb("Выключены ❌", data="settings_status_work:True")
if get_settings.status_refill == "False":
status_refill_kb = ikb("Выключены ❌", data="settings_status_refill:True")
+ if get_settings.status_referral == "False":
+ status_referral_kb = ikb("Выключена ❌", data="settings_status_referral:True")
if get_settings.notification_buy == "False":
notification_buy_kb = ikb("Выключены 🔕", data="settings_notification_buy:True")
if get_settings.notification_refill == "False":
@@ -263,6 +294,9 @@ async def settings_status_finl() -> InlineKeyboardMarkup:
).row(
ikb("🎁 Покупки", data="..."),
status_buy_kb,
+ ).row(
+ ikb("🤝 Реферальная система", data="..."),
+ status_referral_kb,
).row(
ikb("📢 Увед. о покупках", data="..."),
notification_buy_kb,
diff --git a/tgbot/keyboards/inline_user.py b/tgbot/keyboards/inline_user.py
index 9141e17..168c8b4 100644
--- a/tgbot/keyboards/inline_user.py
+++ b/tgbot/keyboards/inline_user.py
@@ -33,6 +33,41 @@ def user_support_finl(support_login: str) -> InlineKeyboardMarkup:
return keyboard.as_markup()
+def referral_menu_finl(referral_link: str) -> InlineKeyboardMarkup:
+ keyboard = InlineKeyboardBuilder()
+
+ keyboard.row(
+ ikb("💸 Перевести на основной баланс", data="referral_transfer"),
+ ).row(
+ ikb("📥 Вывести средства", data="referral_withdrawal"),
+ )
+ keyboard.row(ikb("▪️ Скопировать ссылку", copy=referral_link))
+
+ return keyboard.as_markup()
+
+
+def referral_transfer_method_finl() -> InlineKeyboardMarkup:
+ keyboard = InlineKeyboardBuilder()
+
+ keyboard.row(ikb("🔙 Вернуться", data="referral_menu"))
+
+ return keyboard.as_markup()
+
+
+def referral_withdrawal_method_finl() -> InlineKeyboardMarkup:
+ keyboard = InlineKeyboardBuilder()
+
+ keyboard.row(
+ ikb("🔷 CryptoBot", data="referral_withdrawal_method:Cryptobot"),
+ ).row(
+ ikb("🟢 Lolzteam", data="referral_withdrawal_method:Lolzteam"),
+ ).row(
+ ikb("🔙 Вернуться", data="referral_menu"),
+ )
+
+ return keyboard.as_markup()
+
+
################################################################################
################################### ПЛАТЕЖИ ####################################
# Выбор способа пополнения
diff --git a/tgbot/keyboards/reply_main.py b/tgbot/keyboards/reply_main.py
index 52c68bc..e9e91b7 100644
--- a/tgbot/keyboards/reply_main.py
+++ b/tgbot/keyboards/reply_main.py
@@ -14,6 +14,8 @@ def menu_frep(user_id: int) -> ReplyKeyboardMarkup:
rkb("🎁 Купить"),
rkb("👤 Профиль"),
rkb("🧮 Наличие товаров"),
+ ).row(
+ rkb("🤝 Реферальная система"),
).row(
rkb("☎️ Поддержка"),
rkb("❔ FAQ"),
diff --git a/tgbot/middlewares/middleware_user.py b/tgbot/middlewares/middleware_user.py
index 9f4bd15..8ae2b0d 100644
--- a/tgbot/middlewares/middleware_user.py
+++ b/tgbot/middlewares/middleware_user.py
@@ -3,16 +3,37 @@ from aiogram import BaseMiddleware
from cachetools import TTLCache
from tgbot.data.config import BOT_USER_CACHE_TTL
-from tgbot.database import Userx
+from tgbot.database import Referralx, Settingsx, Userx
from tgbot.utils.const_functions import clear_html
+from tgbot.utils.misc.bot_logging import bot_logger
class ExistsUserMiddleware(BaseMiddleware):
# Создание кеша пользователей
def __init__(self, cache_ttl: int = BOT_USER_CACHE_TTL) -> None:
self.users = Userx()
+ self.referrals = Referralx()
self.cache = TTLCache(maxsize=10_000, ttl=cache_ttl)
+ @staticmethod
+ def _get_referrer_id(event) -> int | None:
+ event_text = getattr(event, "text", None)
+
+ if not isinstance(event_text, str):
+ return None
+
+ start_data = event_text.split(maxsplit=1)
+
+ if len(start_data) != 2 or start_data[0] != "/start":
+ return None
+
+ referral_data = start_data[1]
+
+ if not referral_data.startswith("r_") or not referral_data[2:].isdigit():
+ return None
+
+ return int(referral_data[2:])
+
# Добавление или обновление пользователя перед handler-ом
async def __call__(self, handler, event, data):
this_user = data.get("event_from_user")
@@ -32,6 +53,12 @@ class ExistsUserMiddleware(BaseMiddleware):
)
cached_user = self.cache.get(user_id)
+ existing_user = None
+
+ if cached_user is None:
+ existing_user = await self.users.get(user_id=user_id)
+
+ is_new_user = cached_user is None and existing_user is None
if cached_user is None or cached_user["data"] != user_data:
user = await self.users.upsert(
@@ -41,10 +68,48 @@ class ExistsUserMiddleware(BaseMiddleware):
user_surname=user_data[2],
user_fullname=user_data[3],
)
- self.cache[user_id] = {"data": user_data, "user": user}
else:
user = cached_user["user"]
+ referrer_id = self._get_referrer_id(event)
+
+ if is_new_user and referrer_id is not None:
+ registration_status = await self.referrals.register_referral(
+ referral_id=user_id,
+ referrer_id=referrer_id,
+ )
+
+ if registration_status == "ok":
+ user = await self.users.get_required(user_id=user_id)
+ get_settings = await Settingsx().get()
+ bot = data.get("bot") or getattr(event, "bot", None)
+
+ if bot is not None:
+ notifications = (
+ (
+ referrer_id,
+ "🤝 Новый реферал\n\n"
+ f"▪️ Пользователь: {user.user_name}\n"
+ f"▪️ Начислено: {get_settings.referral_bonus_rub}₽",
+ ),
+ (
+ user_id,
+ "🤝 Реферальный бонус\n\n"
+ f"▪️ Вам начислено: {get_settings.referral_bonus_rub}₽",
+ ),
+ )
+
+ for recipient_id, notification_text in notifications:
+ try:
+ await bot.send_message(recipient_id, notification_text)
+ except Exception:
+ bot_logger.warning(
+ "Не удалось отправить уведомление о реферальной регистрации",
+ exc_info=True,
+ )
+
+ self.cache[user_id] = {"data": user_data, "user": user}
+
data["User"] = user
return await handler(event, data)
diff --git a/tgbot/routers/__init__.py b/tgbot/routers/__init__.py
index 0af4adc..7bc36a1 100644
--- a/tgbot/routers/__init__.py
+++ b/tgbot/routers/__init__.py
@@ -2,8 +2,19 @@
from aiogram import Dispatcher
from tgbot.routers import main_errors, main_start, main_missed
-from tgbot.routers.admin import admin_menu, admin_functions, admin_payments, admin_products, admin_settings
-from tgbot.routers.user import user_menu, user_transactions, user_products
+from tgbot.routers.admin import (
+ admin_menu,
+ admin_functions,
+ admin_payments,
+ admin_products,
+ admin_settings,
+)
+from tgbot.routers.user import (
+ user_menu,
+ user_referral,
+ user_transactions,
+ user_products,
+)
from tgbot.utils.misc.bot_filters import IsAdmin, IsPrivate
@@ -23,17 +34,39 @@ def register_all_routers(dp: Dispatcher):
user_products.router.callback_query.filter(IsPrivate())
user_transactions.router.message.filter(IsPrivate())
user_transactions.router.callback_query.filter(IsPrivate())
+ user_referral.router.message.filter(IsPrivate())
+ user_referral.router.callback_query.filter(IsPrivate())
- admin_menu.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
- admin_menu.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
- admin_functions.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
- admin_functions.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
- admin_payments.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
- admin_payments.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
- admin_settings.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
- admin_settings.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
- admin_products.router.message.filter(IsPrivate(), IsAdmin()) # Работа message роутера только для админов
- admin_products.router.callback_query.filter(IsPrivate(), IsAdmin()) # Работа callback роутера только для админов
+ admin_menu.router.message.filter(
+ IsPrivate(), IsAdmin()
+ ) # Работа message роутера только для админов
+ admin_menu.router.callback_query.filter(
+ IsPrivate(), IsAdmin()
+ ) # Работа callback роутера только для админов
+ admin_functions.router.message.filter(
+ IsPrivate(), IsAdmin()
+ ) # Работа message роутера только для админов
+ admin_functions.router.callback_query.filter(
+ IsPrivate(), IsAdmin()
+ ) # Работа callback роутера только для админов
+ admin_payments.router.message.filter(
+ IsPrivate(), IsAdmin()
+ ) # Работа message роутера только для админов
+ admin_payments.router.callback_query.filter(
+ IsPrivate(), IsAdmin()
+ ) # Работа callback роутера только для админов
+ admin_settings.router.message.filter(
+ IsPrivate(), IsAdmin()
+ ) # Работа message роутера только для админов
+ admin_settings.router.callback_query.filter(
+ IsPrivate(), IsAdmin()
+ ) # Работа callback роутера только для админов
+ admin_products.router.message.filter(
+ IsPrivate(), IsAdmin()
+ ) # Работа message роутера только для админов
+ admin_products.router.callback_query.filter(
+ IsPrivate(), IsAdmin()
+ ) # Работа callback роутера только для админов
# Подключение обязательных роутеров
dp.include_router(main_errors.router) # Роутер ошибки
@@ -44,6 +77,7 @@ def register_all_routers(dp: Dispatcher):
dp.include_router(admin_menu.router) # Админ роутер
dp.include_router(user_products.router) # Юзер роутер
dp.include_router(user_transactions.router) # Юзер роутер
+ dp.include_router(user_referral.router) # Юзер роутер
dp.include_router(admin_functions.router) # Админ роутер
dp.include_router(admin_payments.router) # Админ роутер
dp.include_router(admin_settings.router) # Админ роутер
diff --git a/tgbot/routers/admin/admin_functions.py b/tgbot/routers/admin/admin_functions.py
index 9eafb4c..fdec0bb 100644
--- a/tgbot/routers/admin/admin_functions.py
+++ b/tgbot/routers/admin/admin_functions.py
@@ -5,18 +5,137 @@ from aiogram import Router, Bot, F
from aiogram.filters import StateFilter
from aiogram.types import CallbackQuery, Message
-from tgbot.database import Purchasesx, Refillx, Userx
+from tgbot.database import Purchasesx, Refillx, ReferralWithdrawalx, Userx
from tgbot.keyboards.inline_admin import profile_edit_return_finl, mail_confirm_finl
from tgbot.services.api_hosting_text import HostingAPI
-from tgbot.utils.const_functions import is_number, to_number, del_message, ded, clear_html, convert_date
+from tgbot.utils.const_functions import (
+ is_number,
+ to_number,
+ del_message,
+ ded,
+ clear_html,
+ convert_date,
+)
from tgbot.utils.misc.bot_logging import bot_logger
from tgbot.utils.misc.bot_models import FSM, ARS
from tgbot.utils.misc_functions import functions_mail_make
-from tgbot.utils.text_functions import open_profile_admin, refill_open_admin, purchase_open_admin
+from tgbot.utils.text_functions import (
+ open_profile_admin,
+ refill_open_admin,
+ purchase_open_admin,
+)
router = Router(name=__name__)
+def withdrawal_method_title(withdrawal_method: str) -> str:
+ if withdrawal_method == "Cryptobot":
+ return "CryptoBot"
+
+ return withdrawal_method
+
+
+@router.callback_query(F.data.startswith("referral_withdrawal_complete:"))
+async def referral_withdrawal_complete(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
+ withdrawal_id_raw = call.data.split(":", 1)[1]
+ if not withdrawal_id_raw.isdigit():
+ return await call.answer("❌ Заявка не найдена", True)
+
+ status, withdrawal = await ReferralWithdrawalx().complete(
+ withdrawal_id=int(withdrawal_id_raw),
+ admin_id=call.from_user.id,
+ )
+ if status == "ALREADY_PROCESSED":
+ return await call.answer("❌ Заявка уже обработана", True)
+ if status != "ok" or withdrawal is None:
+ return await call.answer("❌ Не удалось обработать заявку", True)
+
+ method_title = withdrawal_method_title(withdrawal.withdrawal_method)
+ get_user = await Userx().get_required(user_id=withdrawal.user_id)
+ await call.message.edit_text(
+ ded(f"""
+ ✅ Заявка на вывод выполнена
+
+ ▪️ Номер заявки: #{withdrawal.increment}
+ ▪️ Пользователь: @{get_user.user_login} | {get_user.user_name} | {get_user.user_id}
+ ▪️ Сумма: {withdrawal.withdrawal_amount}₽
+ ▪️ Способ: {method_title}
+ ▪️ Реквизиты: {withdrawal.withdrawal_recipient}
+ """),
+ reply_markup=None,
+ )
+ try:
+ await bot.send_message(
+ withdrawal.user_id,
+ ded(f"""
+ ✅ Заявка на вывод выполнена
+
+ ▪️ Номер заявки: #{withdrawal.increment}
+ ▪️ Сумма: {withdrawal.withdrawal_amount}₽
+ ▪️ Способ: {method_title}
+ """),
+ )
+ except Exception:
+ bot_logger.warning(
+ "Не удалось отправить пользователю уведомление о выполненном выводе",
+ exc_info=True,
+ )
+
+ await call.answer("✅ Заявка выполнена", True)
+
+
+@router.callback_query(F.data.startswith("referral_withdrawal_reject:"))
+async def referral_withdrawal_reject(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
+ withdrawal_id_raw = call.data.split(":", 1)[1]
+ if not withdrawal_id_raw.isdigit():
+ return await call.answer("❌ Заявка не найдена", True)
+
+ status, withdrawal = await ReferralWithdrawalx().reject(
+ withdrawal_id=int(withdrawal_id_raw),
+ admin_id=call.from_user.id,
+ )
+ if status == "ALREADY_PROCESSED":
+ return await call.answer("❌ Заявка уже обработана", True)
+ if status != "ok" or withdrawal is None:
+ return await call.answer("❌ Не удалось обработать заявку", True)
+
+ get_user = await Userx().get_required(user_id=withdrawal.user_id)
+ method_title = withdrawal_method_title(withdrawal.withdrawal_method)
+ await call.message.edit_text(
+ ded(f"""
+ ❌ Заявка на вывод отклонена
+
+ ▪️ Номер заявки: #{withdrawal.increment}
+ ▪️ Пользователь: @{get_user.user_login} | {get_user.user_name} | {get_user.user_id}
+ ▪️ Сумма возвращена на реферальный баланс: {withdrawal.withdrawal_amount}₽
+ ▪️ Способ: {method_title}
+ ▪️ Реквизиты: {withdrawal.withdrawal_recipient}
+ """),
+ reply_markup=None,
+ )
+ try:
+ await bot.send_message(
+ withdrawal.user_id,
+ ded(f"""
+ ❌ Заявка на вывод отклонена
+
+ ▪️ Номер заявки: #{withdrawal.increment}
+ ▪️ Сумма возвращена на реферальный баланс: {withdrawal.withdrawal_amount}₽
+ """),
+ )
+ except Exception:
+ bot_logger.warning(
+ "Не удалось отправить пользователю уведомление об отклонённом выводе",
+ exc_info=True,
+ )
+
+ await call.answer("❌ Заявка отклонена", True)
+
+
# Поиск чеков и профилей
@router.message(F.text == "🔍 Поиск")
async def functions_find(message: Message, bot: Bot, state: FSM, arSession: ARS):
@@ -55,17 +174,23 @@ async def functions_mail_get(message: Message, bot: Bot, state: FSM, arSession:
# Подтверждение отправки рассылки
-@router.callback_query(F.data.startswith("mail_confirm:"), StateFilter("here_mail_confirm"))
-async def functions_mail_confirm(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
+@router.callback_query(
+ F.data.startswith("mail_confirm:"), StateFilter("here_mail_confirm")
+)
+async def functions_mail_confirm(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
get_status = call.data.split(":")[1]
- send_message = (await state.get_data())['here_mail_message']
+ send_message = (await state.get_data())["here_mail_message"]
await state.clear()
if get_status == "Yes":
get_users = await Userx().get_all()
- await call.message.edit_text(f"📢 Рассылка началась... (0/{len(get_users)})")
+ await call.message.edit_text(
+ f"📢 Рассылка началась... (0/{len(get_users)})"
+ )
await asyncio.create_task(functions_mail_make(bot, send_message, call))
else:
@@ -76,7 +201,7 @@ async def functions_mail_confirm(call: CallbackQuery, bot: Bot, state: FSM, arSe
##################################### ПОИСК ####################################
# Принятие айди/логина пользователя или чека для поиска
@router.message(F.text, StateFilter("here_find"))
-@router.message(F.text.lower().startswith(('.find', 'find')))
+@router.message(F.text.lower().startswith((".find", "find")))
async def functions_find_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
find_data = message.text.lower()
@@ -116,14 +241,18 @@ async def functions_find_get(message: Message, bot: Bot, state: FSM, arSession:
return await refill_open_admin(bot, message.from_user.id, get_refill)
if get_purchase is not None:
- return await purchase_open_admin(bot, arSession, message.from_user.id, get_purchase)
+ return await purchase_open_admin(
+ bot, arSession, message.from_user.id, get_purchase
+ )
################################################################################
############################## УПРАВЛЕНИЕ ПРОФИЛЕМ #############################
# Обновление профиля пользователя
@router.callback_query(F.data.startswith("admin_user_refresh:"))
-async def functions_user_refresh(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
+async def functions_user_refresh(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
user_id = int(call.data.split(":")[1])
get_user = await Userx().get_required(user_id=user_id)
@@ -136,7 +265,9 @@ async def functions_user_refresh(call: CallbackQuery, bot: Bot, state: FSM, arSe
# Покупки пользователя
@router.callback_query(F.data.startswith("admin_user_purchases:"))
-async def functions_user_purchases(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
+async def functions_user_purchases(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
user_id = int(call.data.split(":")[1])
get_user = await Userx().get_required(user_id=user_id)
@@ -174,7 +305,9 @@ async def functions_user_purchases(call: CallbackQuery, bot: Bot, state: FSM, ar
# Выдача баланса пользователю
@router.callback_query(F.data.startswith("admin_user_balance_add:"))
-async def functions_user_balance_add(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
+async def functions_user_balance_add(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
user_id = int(call.data.split(":")[1])
await state.update_data(here_user=user_id)
@@ -188,8 +321,10 @@ async def functions_user_balance_add(call: CallbackQuery, bot: Bot, state: FSM,
# Принятие суммы для выдачи баланса пользователю
@router.message(F.text, StateFilter("here_user_add"))
-async def functions_user_balance_add_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
- user_id = (await state.get_data())['here_user']
+async def functions_user_balance_add_get(
+ message: Message, bot: Bot, state: FSM, arSession: ARS
+):
+ user_id = (await state.get_data())["here_user"]
if not is_number(message.text):
return await message.answer(
@@ -222,7 +357,11 @@ async def functions_user_balance_add_get(message: Message, bot: Bot, state: FSM,
f"💰 Вам было выдано {message.text}₽",
)
except Exception:
- bot_logger.debug("Не удалось уведомить пользователя %s о выдаче баланса", user_id, exc_info=True)
+ bot_logger.debug(
+ "Не удалось уведомить пользователя %s о выдаче баланса",
+ user_id,
+ exc_info=True,
+ )
await message.answer(
f"👤 Пользователь: {get_user.user_name}\n"
@@ -235,7 +374,9 @@ async def functions_user_balance_add_get(message: Message, bot: Bot, state: FSM,
# Изменение баланса пользователю
@router.callback_query(F.data.startswith("admin_user_balance_set:"))
-async def functions_user_balance_set(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
+async def functions_user_balance_set(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
user_id = int(call.data.split(":")[1])
await state.update_data(here_user=user_id)
@@ -249,8 +390,10 @@ async def functions_user_balance_set(call: CallbackQuery, bot: Bot, state: FSM,
# Принятие суммы для изменения баланса пользователя
@router.message(F.text, StateFilter("here_user_set"))
-async def functions_user_balance_set_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
- user_id = (await state.get_data())['here_user']
+async def functions_user_balance_set_get(
+ message: Message, bot: Bot, state: FSM, arSession: ARS
+):
+ user_id = (await state.get_data())["here_user"]
if not is_number(message.text):
return await message.answer(
@@ -294,7 +437,9 @@ async def functions_user_balance_set_get(message: Message, bot: Bot, state: FSM,
# Отправка сообщения пользователю
@router.callback_query(F.data.startswith("admin_user_message:"))
-async def functions_user_user_message(call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS):
+async def functions_user_user_message(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
user_id = int(call.data.split(":")[1])
await state.update_data(here_user_id=user_id)
@@ -309,17 +454,24 @@ async def functions_user_user_message(call: CallbackQuery, bot: Bot, state: FSM,
# Принятие сообщения для отправки пользователю
@router.message(F.text, StateFilter("here_user_message"))
-async def functions_user_user_message_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
- user_id = (await state.get_data())['here_user_id']
+async def functions_user_user_message_get(
+ message: Message, bot: Bot, state: FSM, arSession: ARS
+):
+ user_id = (await state.get_data())["here_user_id"]
await state.clear()
- get_message = "💌 Сообщение от администратора:\n" + f"{clear_html(message.text)}"
+ get_message = (
+ "💌 Сообщение от администратора:\n"
+ + f"{clear_html(message.text)}"
+ )
get_user = await Userx().get_required(user_id=user_id)
try:
await bot.send_message(user_id, get_message)
except Exception:
- bot_logger.debug("Не удалось отправить сообщение пользователю %s", user_id, exc_info=True)
+ bot_logger.debug(
+ "Не удалось отправить сообщение пользователю %s", user_id, exc_info=True
+ )
await message.reply("❌ Не удалось отправить сообщение")
else:
await message.reply("✅ Сообщение было успешно доставлено")
diff --git a/tgbot/routers/admin/admin_settings.py b/tgbot/routers/admin/admin_settings.py
index afe3d81..c72d03f 100644
--- a/tgbot/routers/admin/admin_settings.py
+++ b/tgbot/routers/admin/admin_settings.py
@@ -1,11 +1,13 @@
# - *- coding: utf- 8 - *-
+from math import isfinite
+
from aiogram import Router, Bot, F
from aiogram.filters import StateFilter
from aiogram.types import CallbackQuery, Message
from tgbot.database import Settingsx, Userx
from tgbot.keyboards.inline_admin import settings_status_finl, settings_finl
-from tgbot.utils.const_functions import ded
+from tgbot.utils.const_functions import ded, is_number, to_number
from tgbot.utils.misc.bot_logging import bot_logger
from tgbot.utils.misc.bot_models import FSM, ARS
from tgbot.utils.misc_functions import send_admins, insert_tags
@@ -116,6 +118,31 @@ async def settings_status_refill(
await call.message.edit_reply_markup(reply_markup=await settings_status_finl())
+# Включение/выключение реферальной системы
+@router.callback_query(F.data.startswith("settings_status_referral:"))
+async def settings_status_referral(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
+ get_status = call.data.split(":")[1]
+
+ get_user = await Userx().get_required(user_id=call.from_user.id)
+ await Settingsx().update(status_referral=get_status)
+
+ if get_status == "True":
+ send_text = "🟢 Включил реферальную систему в боте"
+ else:
+ send_text = "🔴 Выключил реферальную систему в боте"
+
+ await send_admins(
+ bot,
+ f"👤 Администратор {get_user.user_name}\n"
+ f"{send_text}",
+ not_me=get_user.user_id,
+ )
+
+ await call.message.edit_reply_markup(reply_markup=await settings_status_finl())
+
+
# Включение/выключение уведомлений о покупках
@router.callback_query(F.data.startswith("settings_notification_buy:"))
async def settings_notification_buy(
@@ -267,6 +294,72 @@ async def settings_faq_get(message: Message, bot: Bot, state: FSM, arSession: AR
# Принятие поддержки
+@router.callback_query(F.data == "settings_edit_referral_bonus")
+async def settings_edit_referral_bonus(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
+ await state.clear()
+ await state.set_state("here_settings_referral_bonus")
+ await call.message.edit_text(
+ "🤝 Введите фиксированный бонус за приглашение в рублях"
+ )
+
+
+@router.callback_query(F.data == "settings_edit_referral_percent")
+async def settings_edit_referral_percent(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
+ await state.clear()
+ await state.set_state("here_settings_referral_percent")
+ await call.message.edit_text(
+ "📈 Введите процент дохода с пополнений от 0 до 100"
+ )
+
+
+@router.message(F.text, StateFilter("here_settings_referral_bonus"))
+async def settings_referral_bonus_get(
+ message: Message, bot: Bot, state: FSM, arSession: ARS
+):
+ if not is_number(message.text):
+ return await message.answer(
+ "❌ Введите неотрицательное число в рублях"
+ )
+
+ referral_bonus = float(to_number(message.text))
+
+ if not isfinite(referral_bonus) or referral_bonus < 0:
+ return await message.answer(
+ "❌ Введите неотрицательное число в рублях"
+ )
+
+ await Settingsx().update(referral_bonus_rub=round(referral_bonus, 2))
+ await state.clear()
+ await message.answer(
+ "🖍 Изменение данных бота",
+ reply_markup=await settings_finl(),
+ )
+
+
+@router.message(F.text, StateFilter("here_settings_referral_percent"))
+async def settings_referral_percent_get(
+ message: Message, bot: Bot, state: FSM, arSession: ARS
+):
+ if not is_number(message.text):
+ return await message.answer("❌ Введите число от 0 до 100")
+
+ referral_percent = float(to_number(message.text))
+
+ if not isfinite(referral_percent) or not 0 <= referral_percent <= 100:
+ return await message.answer("❌ Введите число от 0 до 100")
+
+ await Settingsx().update(referral_refill_percent=round(referral_percent, 2))
+ await state.clear()
+ await message.answer(
+ "🖍 Изменение данных бота",
+ reply_markup=await settings_finl(),
+ )
+
+
@router.message(F.text, StateFilter("here_settings_support"))
async def settings_support_get(message: Message, bot: Bot, state: FSM, arSession: ARS):
get_support = message.text
diff --git a/tgbot/routers/main_start.py b/tgbot/routers/main_start.py
index 14f7946..19e311e 100644
--- a/tgbot/routers/main_start.py
+++ b/tgbot/routers/main_start.py
@@ -124,7 +124,9 @@ async def main_start(message: Message, bot: Bot, state: FSM, arSession: ARS):
async def main_start_deeplink(message: Message, bot: Bot, state: FSM, arSession: ARS):
deepling_args = message.text[7:]
- if deepling_args.startswith("p_"):
+ if deepling_args.startswith("r_"):
+ await main_start(message, bot, state, arSession)
+ elif deepling_args.startswith("p_"):
position_id_raw = deepling_args[2:]
if not position_id_raw.isdigit():
diff --git a/tgbot/routers/user/user_referral.py b/tgbot/routers/user/user_referral.py
new file mode 100644
index 0000000..8e3b313
--- /dev/null
+++ b/tgbot/routers/user/user_referral.py
@@ -0,0 +1,359 @@
+# - *- coding: utf- 8 - *-
+from math import isfinite
+from urllib.parse import urlparse
+import re
+
+from aiogram import Bot, F, Router
+from aiogram.filters import StateFilter
+from aiogram.types import CallbackQuery, Message
+
+from tgbot.database import ReferralWithdrawalx, Referralx, Settingsx, Userx
+from tgbot.keyboards.inline_admin import referral_withdrawal_actions_finl
+from tgbot.keyboards.inline_user import (
+ referral_menu_finl,
+ referral_transfer_method_finl,
+ referral_withdrawal_method_finl,
+)
+from tgbot.utils.const_functions import clear_html, ded, is_number, to_number
+from tgbot.utils.misc.bot_models import ARS, FSM
+from tgbot.utils.misc_functions import send_admins
+
+router = Router(name=__name__)
+
+
+def is_valid_withdrawal_recipient(withdrawal_method: str, recipient: str) -> bool:
+ if withdrawal_method == "Cryptobot":
+ return bool(re.fullmatch(r"@[A-Za-z][A-Za-z0-9_]{4,31}", recipient))
+
+ if withdrawal_method == "Lolzteam":
+ parsed_url = urlparse(recipient)
+ return parsed_url.scheme in ("http", "https") and parsed_url.hostname in (
+ "zelenka.guru",
+ "lolz.team",
+ "lolz.live",
+ "lolz.guru",
+ )
+
+ return False
+
+
+def withdrawal_recipient_hint(withdrawal_method: str) -> str:
+ if withdrawal_method == "Cryptobot":
+ return (
+ "📥 Введите @username получателя в CryptoBot\n"
+ "➖➖➖➖➖➖➖➖➖➖\n"
+ "Пример: @username"
+ )
+
+ return (
+ "📥 Введите ссылку на профиль получателя Lolzteam\n"
+ "➖➖➖➖➖➖➖➖➖➖\n"
+ "Подойдут ссылки с zelenka.guru, lolz.team, lolz.live или lolz.guru"
+ )
+
+
+def withdrawal_method_title(withdrawal_method: str) -> str:
+ if withdrawal_method == "Cryptobot":
+ return "CryptoBot"
+
+ return withdrawal_method
+
+
+async def get_referral_menu_data(user_id: int, bot: Bot):
+ get_settings = await Settingsx().get()
+
+ if get_settings.status_referral != "True":
+ return None
+
+ get_user = await Userx().get_required(user_id=user_id)
+ get_referrals = await Referralx().gets(referrer_id=user_id)
+
+ bot_username = get_settings.misc_bot
+
+ if bot_username == "None":
+ bot_username = (await bot.get_me()).username or ""
+
+ referral_link = f"https://t.me/{bot_username}?start=r_{user_id}"
+ available_balance = round(
+ get_user.user_referral_balance - get_user.user_referral_hold,
+ 2,
+ )
+
+ referral_text = ded(f"""
+ 🤝 Реферальная система
+ ➖➖➖➖➖➖➖➖➖➖
+ ▪️ Приглашено пользователей: {len(get_referrals)}
+ ▪️ Реферальный баланс: {get_user.user_referral_balance}₽
+ ▪️ Доступно: {available_balance}₽
+ ➖➖➖➖➖➖➖➖➖➖
+ ▪️ Бонус за приглашение: {get_settings.referral_bonus_rub}₽
+ ▪️ Доход с пополнений: {get_settings.referral_refill_percent}%
+ """)
+
+ return referral_text, referral_menu_finl(referral_link)
+
+
+@router.message(F.text == "🤝 Реферальная система")
+async def show_referral_menu(
+ message: Message,
+ bot: Bot,
+ state: FSM,
+ arSession: ARS,
+ user_id: int | None = None,
+):
+ await state.clear()
+
+ user_id = user_id or message.from_user.id
+ referral_menu_data = await get_referral_menu_data(user_id, bot)
+ if referral_menu_data is None:
+ return await message.answer("⛔ Реферальная система временно отключена")
+
+ referral_text, referral_keyboard = referral_menu_data
+
+ await message.answer(
+ referral_text,
+ reply_markup=referral_keyboard,
+ disable_web_page_preview=True,
+ )
+
+
+@router.callback_query(F.data == "referral_menu")
+async def user_referral_menu_return(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
+ await state.clear()
+ referral_menu_data = await get_referral_menu_data(call.from_user.id, bot)
+ if referral_menu_data is None:
+ return await call.answer("⛔ Реферальная система временно отключена", True)
+
+ referral_text, referral_keyboard = referral_menu_data
+ await call.message.edit_text(
+ referral_text,
+ reply_markup=referral_keyboard,
+ disable_web_page_preview=True,
+ )
+
+
+@router.callback_query(F.data == "referral_transfer")
+async def user_referral_transfer_start(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
+ get_settings = await Settingsx().get()
+
+ if get_settings.status_referral != "True":
+ await state.clear()
+ return await call.answer("⛔ Реферальная система временно отключена", True)
+
+ await state.clear()
+ await state.set_state("here_referral_transfer_amount")
+ await call.message.edit_text(
+ "💸 Введите сумму для перевода на основной баланс",
+ reply_markup=referral_transfer_method_finl(),
+ )
+
+
+@router.callback_query(F.data == "referral_withdrawal")
+async def user_referral_withdrawal_start(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
+ get_settings = await Settingsx().get()
+ if get_settings.status_referral != "True":
+ await state.clear()
+ return await call.answer("⛔ Реферальная система временно отключена", True)
+
+ await state.clear()
+ await call.message.edit_text(
+ "📥 Выберите способ вывода средств",
+ reply_markup=referral_withdrawal_method_finl(),
+ )
+
+
+@router.callback_query(F.data.startswith("referral_withdrawal_method:"))
+async def user_referral_withdrawal_method(
+ call: CallbackQuery, bot: Bot, state: FSM, arSession: ARS
+):
+ withdrawal_method = call.data.split(":", 1)[1]
+ if withdrawal_method not in ("Cryptobot", "Lolzteam"):
+ return await call.answer("❌ Способ вывода не найден", True)
+
+ get_settings = await Settingsx().get()
+ if get_settings.status_referral != "True":
+ await state.clear()
+ return await call.answer("⛔ Реферальная система временно отключена", True)
+
+ await state.clear()
+ await state.update_data(here_referral_withdrawal_method=withdrawal_method)
+ await state.set_state("here_referral_withdrawal_amount")
+ await call.message.edit_text("📥 Введите сумму для вывода")
+
+
+@router.message(F.text, StateFilter("here_referral_withdrawal_amount"))
+async def user_referral_withdrawal_amount(
+ message: Message, bot: Bot, state: FSM, arSession: ARS
+):
+ get_settings = await Settingsx().get()
+ if get_settings.status_referral != "True":
+ await state.clear()
+ return await message.answer("⛔ Реферальная система временно отключена")
+
+ if not is_number(message.text):
+ return await message.answer("❌ Введите положительную сумму в рублях")
+
+ withdrawal_amount = float(to_number(message.text))
+ if not isfinite(withdrawal_amount) or withdrawal_amount <= 0:
+ return await message.answer("❌ Введите положительную сумму в рублях")
+
+ get_user = await Userx().get_required(user_id=message.from_user.id)
+ available_balance = round(
+ get_user.user_referral_balance - get_user.user_referral_hold, 2
+ )
+ if withdrawal_amount > available_balance:
+ return await message.answer(
+ "❌ Недостаточно доступных средств на реферальном балансе"
+ )
+
+ withdrawal_data = await state.get_data()
+ withdrawal_method = withdrawal_data.get("here_referral_withdrawal_method")
+ if withdrawal_method not in ("Cryptobot", "Lolzteam"):
+ await state.clear()
+ return await message.answer("❌ Данные вывода устарели. Начните заново")
+
+ await state.update_data(here_referral_withdrawal_amount=withdrawal_amount)
+ await state.set_state("here_referral_withdrawal_recipient")
+ await message.answer(withdrawal_recipient_hint(withdrawal_method))
+
+
+@router.message(F.text, StateFilter("here_referral_withdrawal_recipient"))
+async def user_referral_withdrawal_recipient(
+ message: Message, bot: Bot, state: FSM, arSession: ARS
+):
+ get_settings = await Settingsx().get()
+ if get_settings.status_referral != "True":
+ await state.clear()
+ return await message.answer("⛔ Реферальная система временно отключена")
+
+ withdrawal_data = await state.get_data()
+ withdrawal_method = withdrawal_data.get("here_referral_withdrawal_method")
+ withdrawal_amount = withdrawal_data.get("here_referral_withdrawal_amount")
+
+ if withdrawal_method is None or withdrawal_amount is None:
+ await state.clear()
+ return await message.answer("❌ Данные вывода устарели. Начните заново")
+
+ withdrawal_recipient = clear_html(message.text).strip()
+ if not withdrawal_recipient:
+ return await message.answer(
+ "❌ Введите реквизиты\n"
+ f"{withdrawal_recipient_hint(withdrawal_method)}"
+ )
+
+ if not is_valid_withdrawal_recipient(withdrawal_method, withdrawal_recipient):
+ return await message.answer(
+ "❌ Некорректные реквизиты\n"
+ f"{withdrawal_recipient_hint(withdrawal_method)}"
+ )
+
+ withdrawal_status, withdrawal = await ReferralWithdrawalx().create_pending(
+ user_id=message.from_user.id,
+ withdrawal_amount=withdrawal_amount,
+ withdrawal_method=withdrawal_method,
+ withdrawal_recipient=withdrawal_recipient,
+ )
+
+ if withdrawal_status == "INSUFFICIENT_FUNDS":
+ return await message.answer(
+ "❌ Недостаточно доступных средств на реферальном балансе"
+ )
+
+ if withdrawal_status in ("INVALID_AMOUNT", "INVALID_RECIPIENT", "INVALID_METHOD"):
+ return await message.answer("❌ Некорректные данные для вывода")
+
+ if withdrawal_status == "USER_NOT_FOUND":
+ await state.clear()
+ return await message.answer(
+ "❌ Пользователь не найден. Обратитесь в поддержку"
+ )
+
+ if withdrawal_status != "ok" or withdrawal is None:
+ return await message.answer(
+ "❌ Не удалось создать заявку. Попробуйте позже"
+ )
+
+ await state.clear()
+ get_user = await Userx().get_required(user_id=message.from_user.id)
+ method_title = withdrawal_method_title(withdrawal.withdrawal_method)
+ await message.answer(
+ ded(f"""
+ 📥 Заявка на вывод создана
+ ➖➖➖➖➖➖➖➖➖➖
+ ▪️ Номер заявки: #{withdrawal.increment}
+ ▪️ Сумма: {withdrawal.withdrawal_amount}₽
+ ▪️ Способ: {method_title}
+ """),
+ )
+ await send_admins(
+ bot,
+ ded(f"""
+ 📥 Новая заявка на вывод
+
+ ▪️ Номер заявки: #{withdrawal.increment}
+ ▪️ Пользователь: @{get_user.user_login} | {get_user.user_name} | {get_user.user_id}
+ ▪️ Сумма: {withdrawal.withdrawal_amount}₽
+ ▪️ Способ: {method_title}
+ ▪️ Реквизиты: {withdrawal.withdrawal_recipient}
+ """),
+ keyboard=referral_withdrawal_actions_finl(withdrawal.increment),
+ )
+
+
+@router.message(F.text, StateFilter("here_referral_transfer_amount"))
+async def user_referral_transfer_amount(
+ message: Message, bot: Bot, state: FSM, arSession: ARS
+):
+ get_settings = await Settingsx().get()
+
+ if get_settings.status_referral != "True":
+ await state.clear()
+ return await message.answer("⛔ Реферальная система временно отключена")
+
+ if not is_number(message.text):
+ return await message.answer("❌ Введите положительную сумму в рублях")
+
+ transfer_amount = float(to_number(message.text))
+
+ if not isfinite(transfer_amount) or transfer_amount <= 0:
+ return await message.answer("❌ Введите положительную сумму в рублях")
+
+ transfer_status = await Referralx().transfer_to_main_balance(
+ user_id=message.from_user.id,
+ amount=transfer_amount,
+ )
+
+ if transfer_status == "INSUFFICIENT_FUNDS":
+ return await message.answer(
+ "❌ Недостаточно доступных средств на реферальном балансе"
+ )
+ if transfer_status == "INVALID_AMOUNT":
+ return await message.answer("❌ Введите положительную сумму в рублях")
+ if transfer_status == "USER_NOT_FOUND":
+ await state.clear()
+ return await message.answer(
+ "❌ Пользователь не найден. Обратитесь в поддержку"
+ )
+ if transfer_status != "ok":
+ return await message.answer(
+ "❌ Не удалось выполнить перевод. Попробуйте позже"
+ )
+
+ await state.clear()
+ get_user = await Userx().get_required(user_id=message.from_user.id)
+
+ await message.answer(
+ ded(f"""
+ 💸 Средства переведены на основной баланс
+ ➖➖➖➖➖➖➖➖➖➖
+ ▪️ Переведено: {round(transfer_amount, 2)}₽
+ ▪️ Основной баланс: {get_user.user_balance}₽
+ ▪️ Реферальный баланс: {get_user.user_referral_balance}₽
+ """),
+ )
diff --git a/tgbot/routers/user/user_transactions.py b/tgbot/routers/user/user_transactions.py
index 4d0b497..f43b6ac 100644
--- a/tgbot/routers/user/user_transactions.py
+++ b/tgbot/routers/user/user_transactions.py
@@ -5,7 +5,7 @@ from aiogram import Router, Bot, F
from aiogram.filters import StateFilter
from aiogram.types import CallbackQuery, Message, PreCheckoutQuery
-from tgbot.database import Paymentsx, Refillx, Userx, Settingsx
+from tgbot.database import Paymentsx, ReferralTransactionx, Refillx, Userx, Settingsx
from tgbot.keyboards.inline_user import refill_bill_finl, refill_method_finl
from tgbot.services.api_cryptobot import CryptobotAPI
from tgbot.services.api_lolzteam import LolzteamAPI
@@ -459,4 +459,26 @@ async def save_refill_success(
"""),
)
+ referral_reward = await ReferralTransactionx().get_refill_reward(
+ f"refill:{pay_comment or pay_receipt}"
+ )
+
+ if referral_reward is not None:
+ try:
+ await bot.send_message(
+ referral_reward.user_id,
+ ded(f"""
+ 🤝 Реферальное начисление
+
+ ▪️ Ваш реферал: {get_user.user_name}
+ ▪️ Пополнение: {pay_amount}₽
+ ▪️ Начислено: {referral_reward.amount}₽
+ """),
+ )
+ except Exception:
+ bot_logger.warning(
+ "Не удалось отправить уведомление о реферальном начислении",
+ exc_info=True,
+ )
+
return response_success, pay_receipt