From a04734b8a7611f9a468a7571807f14e920267653 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 18 Jul 2026 10:58:14 +0500 Subject: [PATCH] feat(referral): add referral system with withdrawals --- tgbot/database/__init__.py | 28 +- tgbot/database/db_referral.py | 728 ++++++++++++++++++++++++ tgbot/database/db_refill.py | 70 ++- tgbot/database/db_settings.py | 11 +- tgbot/database/db_users.py | 50 +- tgbot/database/entities.py | 42 ++ tgbot/database/repository.py | 3 + tgbot/keyboards/inline_admin.py | 34 ++ tgbot/keyboards/inline_user.py | 35 ++ tgbot/keyboards/reply_main.py | 2 + tgbot/middlewares/middleware_user.py | 69 ++- tgbot/routers/__init__.py | 58 +- tgbot/routers/admin/admin_functions.py | 198 ++++++- tgbot/routers/admin/admin_settings.py | 95 +++- tgbot/routers/main_start.py | 4 +- tgbot/routers/user/user_referral.py | 359 ++++++++++++ tgbot/routers/user/user_transactions.py | 24 +- 17 files changed, 1750 insertions(+), 60 deletions(-) create mode 100644 tgbot/database/db_referral.py create mode 100644 tgbot/routers/user/user_referral.py 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