mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-05-02 02:36:26 +00:00
касса и прочее
This commit is contained in:
@@ -284,6 +284,19 @@ async def get_payment_methods():
|
||||
)
|
||||
)
|
||||
|
||||
# KassaAI
|
||||
if settings.is_kassa_ai_enabled():
|
||||
methods.append(
|
||||
PaymentMethodResponse(
|
||||
id='kassa_ai',
|
||||
name=settings.get_kassa_ai_display_name(),
|
||||
description='Pay via KassaAI',
|
||||
min_amount_kopeks=settings.KASSA_AI_MIN_AMOUNT_KOPEKS,
|
||||
max_amount_kopeks=settings.KASSA_AI_MAX_AMOUNT_KOPEKS,
|
||||
is_available=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Tribute
|
||||
if settings.TRIBUTE_ENABLED and settings.TRIBUTE_DONATE_LINK:
|
||||
methods.append(
|
||||
@@ -728,6 +741,31 @@ async def create_topup(
|
||||
detail='Failed to create FreeKassa payment',
|
||||
)
|
||||
|
||||
elif request.payment_method == 'kassa_ai':
|
||||
if not settings.is_kassa_ai_enabled():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='KassaAI payment method is unavailable',
|
||||
)
|
||||
|
||||
payment_service = PaymentService()
|
||||
result = await payment_service.create_kassa_ai_payment(
|
||||
db=db,
|
||||
user_id=user.id,
|
||||
amount_kopeks=request.amount_kopeks,
|
||||
description=settings.get_balance_payment_description(request.amount_kopeks),
|
||||
language=getattr(user, 'language', None) or settings.DEFAULT_LANGUAGE,
|
||||
)
|
||||
|
||||
if result and result.get('payment_url'):
|
||||
payment_url = result.get('payment_url')
|
||||
payment_id = str(result.get('local_payment_id') or result.get('order_id') or 'pending')
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail='Failed to create KassaAI payment',
|
||||
)
|
||||
|
||||
elif request.payment_method == 'tribute':
|
||||
if not settings.TRIBUTE_ENABLED or not settings.TRIBUTE_DONATE_LINK:
|
||||
raise HTTPException(
|
||||
@@ -868,6 +906,17 @@ def _get_status_info(record: PendingPayment) -> tuple[str, str]:
|
||||
}
|
||||
return mapping.get(status, ('❓', 'Неизвестно'))
|
||||
|
||||
if record.method == PaymentMethod.KASSA_AI:
|
||||
mapping = {
|
||||
'pending': ('⏳', 'Ожидает оплаты'),
|
||||
'success': ('✅', 'Оплачено'),
|
||||
'paid': ('✅', 'Оплачено'),
|
||||
'canceled': ('❌', 'Отменено'),
|
||||
'failed': ('❌', 'Ошибка'),
|
||||
'expired': ('⌛', 'Истёк'),
|
||||
}
|
||||
return mapping.get(status, ('❓', 'Неизвестно'))
|
||||
|
||||
return '❓', 'Неизвестно'
|
||||
|
||||
|
||||
@@ -896,6 +945,8 @@ def _is_checkable(record: PendingPayment) -> bool:
|
||||
return status in {'pending', 'authorized'}
|
||||
if record.method == PaymentMethod.FREEKASSA:
|
||||
return status in {'pending', 'created', 'processing'}
|
||||
if record.method == PaymentMethod.KASSA_AI:
|
||||
return status in {'pending', 'created', 'processing'}
|
||||
return False
|
||||
|
||||
|
||||
@@ -919,7 +970,7 @@ def _get_payment_url(record: PendingPayment) -> str | None:
|
||||
)
|
||||
elif record.method == PaymentMethod.PLATEGA:
|
||||
payment_url = getattr(payment, 'redirect_url', None) or payment_url
|
||||
elif record.method == PaymentMethod.CLOUDPAYMENTS or record.method == PaymentMethod.FREEKASSA:
|
||||
elif record.method in (PaymentMethod.CLOUDPAYMENTS, PaymentMethod.FREEKASSA, PaymentMethod.KASSA_AI):
|
||||
payment_url = getattr(payment, 'payment_url', None) or payment_url
|
||||
|
||||
return payment_url
|
||||
|
||||
@@ -9,6 +9,7 @@ from sqlalchemy.orm import selectinload
|
||||
from app.database.models import (
|
||||
ReferralContest,
|
||||
ReferralContestEvent,
|
||||
ReferralContestVirtualParticipant,
|
||||
Transaction,
|
||||
TransactionType,
|
||||
User,
|
||||
@@ -617,9 +618,7 @@ async def debug_contest_transactions(
|
||||
|
||||
# Подсчёт общих сумм ПО ТИПАМ (исключаем бонусы без payment_method)
|
||||
deposit_in_period = sum(
|
||||
tx.amount_kopeks
|
||||
for tx in txs_in
|
||||
if tx.type == TransactionType.DEPOSIT.value and tx.payment_method is not None
|
||||
tx.amount_kopeks for tx in txs_in if tx.type == TransactionType.DEPOSIT.value and tx.payment_method is not None
|
||||
)
|
||||
subscription_in_period = sum(
|
||||
tx.amount_kopeks for tx in txs_in if tx.type == TransactionType.SUBSCRIPTION_PAYMENT.value
|
||||
@@ -920,3 +919,96 @@ async def cleanup_invalid_contest_events(
|
||||
'contest_start': contest_start.isoformat(),
|
||||
'contest_end': contest_end.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# ── Виртуальные участники ──────────────────────────────────────────────
|
||||
|
||||
|
||||
async def add_virtual_participant(
|
||||
db: AsyncSession,
|
||||
contest_id: int,
|
||||
display_name: str,
|
||||
referral_count: int,
|
||||
total_amount_kopeks: int = 0,
|
||||
) -> ReferralContestVirtualParticipant:
|
||||
vp = ReferralContestVirtualParticipant(
|
||||
contest_id=contest_id,
|
||||
display_name=display_name,
|
||||
referral_count=referral_count,
|
||||
total_amount_kopeks=total_amount_kopeks,
|
||||
)
|
||||
db.add(vp)
|
||||
await db.commit()
|
||||
await db.refresh(vp)
|
||||
return vp
|
||||
|
||||
|
||||
async def list_virtual_participants(
|
||||
db: AsyncSession,
|
||||
contest_id: int,
|
||||
) -> Sequence[ReferralContestVirtualParticipant]:
|
||||
result = await db.execute(
|
||||
select(ReferralContestVirtualParticipant)
|
||||
.where(ReferralContestVirtualParticipant.contest_id == contest_id)
|
||||
.order_by(ReferralContestVirtualParticipant.referral_count.desc())
|
||||
)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
async def delete_virtual_participant(
|
||||
db: AsyncSession,
|
||||
participant_id: int,
|
||||
) -> bool:
|
||||
result = await db.execute(
|
||||
select(ReferralContestVirtualParticipant).where(ReferralContestVirtualParticipant.id == participant_id)
|
||||
)
|
||||
vp = result.scalar_one_or_none()
|
||||
if not vp:
|
||||
return False
|
||||
await db.delete(vp)
|
||||
await db.commit()
|
||||
return True
|
||||
|
||||
|
||||
async def update_virtual_participant_count(
|
||||
db: AsyncSession,
|
||||
participant_id: int,
|
||||
referral_count: int,
|
||||
) -> ReferralContestVirtualParticipant | None:
|
||||
result = await db.execute(
|
||||
select(ReferralContestVirtualParticipant).where(ReferralContestVirtualParticipant.id == participant_id)
|
||||
)
|
||||
vp = result.scalar_one_or_none()
|
||||
if not vp:
|
||||
return None
|
||||
vp.referral_count = referral_count
|
||||
await db.commit()
|
||||
await db.refresh(vp)
|
||||
return vp
|
||||
|
||||
|
||||
async def get_contest_leaderboard_with_virtual(
|
||||
db: AsyncSession,
|
||||
contest_id: int,
|
||||
*,
|
||||
limit: int | None = None,
|
||||
) -> list[tuple[str, int, int, bool]]:
|
||||
"""Лидерборд с виртуальными участниками.
|
||||
|
||||
Возвращает список кортежей (display_name, referral_count, total_amount, is_virtual).
|
||||
"""
|
||||
real = await get_contest_leaderboard(db, contest_id)
|
||||
virtual = await list_virtual_participants(db, contest_id)
|
||||
|
||||
merged: list[tuple[str, int, int, bool]] = []
|
||||
for user, score, amount in real:
|
||||
merged.append((user.full_name, score, amount, False))
|
||||
for vp in virtual:
|
||||
merged.append((vp.display_name, vp.referral_count, vp.total_amount_kopeks, True))
|
||||
|
||||
merged.sort(key=lambda x: (-x[1], -x[2]))
|
||||
|
||||
if limit:
|
||||
merged = merged[:limit]
|
||||
|
||||
return merged
|
||||
|
||||
@@ -1529,6 +1529,24 @@ class ReferralContestEvent(Base):
|
||||
)
|
||||
|
||||
|
||||
class ReferralContestVirtualParticipant(Base):
|
||||
__tablename__ = 'referral_contest_virtual_participants'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
contest_id = Column(Integer, ForeignKey('referral_contests.id', ondelete='CASCADE'), nullable=False)
|
||||
display_name = Column(String(255), nullable=False)
|
||||
referral_count = Column(Integer, nullable=False, default=0)
|
||||
total_amount_kopeks = Column(Integer, nullable=False, default=0)
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
|
||||
contest = relationship('ReferralContest')
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<ReferralContestVirtualParticipant id={self.id} name='{self.display_name}' count={self.referral_count}>"
|
||||
)
|
||||
|
||||
|
||||
class ContestTemplate(Base):
|
||||
__tablename__ = 'contest_templates'
|
||||
|
||||
|
||||
@@ -1731,6 +1731,65 @@ async def create_referral_contest_events_table() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def create_referral_contest_virtual_participants_table() -> bool:
|
||||
table_exists = await check_table_exists('referral_contest_virtual_participants')
|
||||
if table_exists:
|
||||
logger.info('Таблица referral_contest_virtual_participants уже существует')
|
||||
return True
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
db_type = await get_database_type()
|
||||
|
||||
if db_type == 'sqlite':
|
||||
await conn.execute(
|
||||
text("""
|
||||
CREATE TABLE referral_contest_virtual_participants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
contest_id INTEGER NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
referral_count INTEGER NOT NULL DEFAULT 0,
|
||||
total_amount_kopeks INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(contest_id) REFERENCES referral_contests(id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
)
|
||||
elif db_type == 'postgresql':
|
||||
await conn.execute(
|
||||
text("""
|
||||
CREATE TABLE referral_contest_virtual_participants (
|
||||
id SERIAL PRIMARY KEY,
|
||||
contest_id INTEGER NOT NULL REFERENCES referral_contests(id) ON DELETE CASCADE,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
referral_count INTEGER NOT NULL DEFAULT 0,
|
||||
total_amount_kopeks INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
)
|
||||
else:
|
||||
await conn.execute(
|
||||
text("""
|
||||
CREATE TABLE referral_contest_virtual_participants (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
contest_id INT NOT NULL,
|
||||
display_name VARCHAR(255) NOT NULL,
|
||||
referral_count INT NOT NULL DEFAULT 0,
|
||||
total_amount_kopeks INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(contest_id) REFERENCES referral_contests(id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
)
|
||||
|
||||
logger.info('✅ Таблица referral_contest_virtual_participants создана')
|
||||
return True
|
||||
except Exception as error:
|
||||
logger.error(f'Ошибка создания таблицы referral_contest_virtual_participants: {error}')
|
||||
return False
|
||||
|
||||
|
||||
async def ensure_referral_contest_summary_columns() -> bool:
|
||||
ok = True
|
||||
for column in ['daily_summary_times', 'last_daily_summary_at']:
|
||||
@@ -4223,33 +4282,33 @@ async def fix_button_click_logs_fk() -> bool:
|
||||
logger.info('🔧 Исправляем FK button_click_logs.user_id: telegram_id -> id')
|
||||
|
||||
# Обнуляем невалидные user_id (которые были internal id, а не telegram_id)
|
||||
await conn.execute(text("""
|
||||
await conn.execute(
|
||||
text("""
|
||||
UPDATE button_click_logs
|
||||
SET user_id = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND user_id NOT IN (SELECT telegram_id FROM users)
|
||||
"""))
|
||||
""")
|
||||
)
|
||||
|
||||
# Удаляем старый FK
|
||||
await conn.execute(text(
|
||||
'ALTER TABLE button_click_logs DROP CONSTRAINT IF EXISTS button_click_logs_user_id_fkey'
|
||||
))
|
||||
await conn.execute(
|
||||
text('ALTER TABLE button_click_logs DROP CONSTRAINT IF EXISTS button_click_logs_user_id_fkey')
|
||||
)
|
||||
|
||||
# Меняем тип колонки и добавляем правильный FK
|
||||
await conn.execute(text(
|
||||
'ALTER TABLE button_click_logs ALTER COLUMN user_id TYPE INTEGER'
|
||||
))
|
||||
await conn.execute(text('ALTER TABLE button_click_logs ALTER COLUMN user_id TYPE INTEGER'))
|
||||
|
||||
# Обнуляем все значения, т.к. они были записаны неправильно
|
||||
await conn.execute(text(
|
||||
'UPDATE button_click_logs SET user_id = NULL'
|
||||
))
|
||||
await conn.execute(text('UPDATE button_click_logs SET user_id = NULL'))
|
||||
|
||||
await conn.execute(text(
|
||||
'ALTER TABLE button_click_logs '
|
||||
'ADD CONSTRAINT button_click_logs_user_id_fkey '
|
||||
'FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL'
|
||||
))
|
||||
await conn.execute(
|
||||
text(
|
||||
'ALTER TABLE button_click_logs '
|
||||
'ADD CONSTRAINT button_click_logs_user_id_fkey '
|
||||
'FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL'
|
||||
)
|
||||
)
|
||||
|
||||
logger.info('✅ FK button_click_logs.user_id исправлен')
|
||||
else:
|
||||
@@ -6395,6 +6454,12 @@ async def run_universal_migration():
|
||||
else:
|
||||
logger.warning('⚠️ Проблемы с таблицей referral_contest_events')
|
||||
|
||||
virtual_participants_ready = await create_referral_contest_virtual_participants_table()
|
||||
if virtual_participants_ready:
|
||||
logger.info('✅ Таблица referral_contest_virtual_participants готова')
|
||||
else:
|
||||
logger.warning('⚠️ Проблемы с таблицей referral_contest_virtual_participants')
|
||||
|
||||
contest_type_ready = await ensure_referral_contest_type_column()
|
||||
if contest_type_ready:
|
||||
logger.info('✅ Колонка contest_type для referral_contests готова')
|
||||
|
||||
@@ -9,15 +9,19 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.database.crud.referral_contest import (
|
||||
add_virtual_participant,
|
||||
create_referral_contest,
|
||||
delete_referral_contest,
|
||||
delete_virtual_participant,
|
||||
get_contest_events_count,
|
||||
get_contest_leaderboard,
|
||||
get_contest_leaderboard_with_virtual,
|
||||
get_referral_contest,
|
||||
get_referral_contests_count,
|
||||
list_referral_contests,
|
||||
list_virtual_participants,
|
||||
toggle_referral_contest,
|
||||
update_referral_contest,
|
||||
update_virtual_participant_count,
|
||||
)
|
||||
from app.keyboards.admin import (
|
||||
get_admin_contests_keyboard,
|
||||
@@ -240,8 +244,10 @@ async def show_contest_details(
|
||||
return
|
||||
|
||||
tz = _ensure_timezone(contest.timezone or settings.TIMEZONE)
|
||||
leaderboard = await get_contest_leaderboard(db, contest.id, limit=5)
|
||||
total_events = await get_contest_events_count(db, contest.id)
|
||||
leaderboard = await get_contest_leaderboard_with_virtual(db, contest.id, limit=5)
|
||||
virtual_list = await list_virtual_participants(db, contest.id)
|
||||
virtual_count = sum(vp.referral_count for vp in virtual_list)
|
||||
total_events = await get_contest_events_count(db, contest.id) + virtual_count
|
||||
|
||||
lines = [
|
||||
f'🏆 <b>{contest.title}</b>',
|
||||
@@ -256,8 +262,9 @@ async def show_contest_details(
|
||||
if leaderboard:
|
||||
lines.append('')
|
||||
lines.append(texts.t('ADMIN_CONTEST_LEADERBOARD_TITLE', '📊 Топ участников:'))
|
||||
for idx, (user, score, _) in enumerate(leaderboard, start=1):
|
||||
lines.append(f'{idx}. {user.full_name} — {score}')
|
||||
for idx, (name, score, _, is_virtual) in enumerate(leaderboard, start=1):
|
||||
virt_mark = ' 👻' if is_virtual else ''
|
||||
lines.append(f'{idx}. {name}{virt_mark} — {score}')
|
||||
|
||||
await callback.message.edit_text(
|
||||
'\n'.join(lines),
|
||||
@@ -427,7 +434,7 @@ async def show_leaderboard(
|
||||
await callback.answer(texts.t('ADMIN_CONTEST_NOT_FOUND', 'Конкурс не найден.'), show_alert=True)
|
||||
return
|
||||
|
||||
leaderboard = await get_contest_leaderboard(db, contest_id, limit=10)
|
||||
leaderboard = await get_contest_leaderboard_with_virtual(db, contest_id, limit=10)
|
||||
if not leaderboard:
|
||||
await callback.answer(texts.t('ADMIN_CONTEST_EMPTY_LEADERBOARD', 'Пока нет участников.'), show_alert=True)
|
||||
return
|
||||
@@ -435,9 +442,9 @@ async def show_leaderboard(
|
||||
lines = [
|
||||
texts.t('ADMIN_CONTEST_LEADERBOARD_TITLE', '📊 Топ участников:'),
|
||||
]
|
||||
for idx, (user, score, _) in enumerate(leaderboard, start=1):
|
||||
user_id_display = user.telegram_id or user.email or f'#{user.id}'
|
||||
lines.append(f'{idx}. {user.full_name} ({user_id_display}) — {score}')
|
||||
for idx, (name, score, _, is_virtual) in enumerate(leaderboard, start=1):
|
||||
virt_mark = ' 👻' if is_virtual else ''
|
||||
lines.append(f'{idx}. {name}{virt_mark} — {score}')
|
||||
|
||||
await callback.message.edit_text(
|
||||
'\n'.join(lines),
|
||||
@@ -676,6 +683,9 @@ async def show_detailed_stats(
|
||||
from app.services.referral_contest_service import referral_contest_service
|
||||
|
||||
stats = await referral_contest_service.get_detailed_contest_stats(db, contest_id)
|
||||
virtual = await list_virtual_participants(db, contest_id)
|
||||
virtual_count = len(virtual)
|
||||
virtual_referrals = sum(vp.referral_count for vp in virtual)
|
||||
|
||||
# Общее сообщение с основной статистикой
|
||||
general_lines = [
|
||||
@@ -693,6 +703,10 @@ async def show_detailed_stats(
|
||||
f' 📥 Пополнения баланса: <b>{stats.get("deposit_total", 0) // 100} руб.</b>',
|
||||
]
|
||||
|
||||
if virtual_count > 0:
|
||||
general_lines.append('')
|
||||
general_lines.append(f'👻 Виртуальных: <b>{virtual_count}</b> (рефералов: {virtual_referrals})')
|
||||
|
||||
await callback.message.edit_text(
|
||||
'\n'.join(general_lines),
|
||||
reply_markup=get_referral_contest_manage_keyboard(
|
||||
@@ -970,6 +984,274 @@ async def debug_contest_transactions(
|
||||
)
|
||||
|
||||
|
||||
# ── Виртуальные участники ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_virtual_participants(
|
||||
callback: types.CallbackQuery,
|
||||
db_user,
|
||||
db: AsyncSession,
|
||||
):
|
||||
contest_id = int(callback.data.split('_')[-1])
|
||||
contest = await get_referral_contest(db, contest_id)
|
||||
if not contest:
|
||||
await callback.answer('Конкурс не найден.', show_alert=True)
|
||||
return
|
||||
|
||||
vps = await list_virtual_participants(db, contest_id)
|
||||
|
||||
lines = [f'👻 <b>Виртуальные участники</b> — {contest.title}', '']
|
||||
if vps:
|
||||
for vp in vps:
|
||||
lines.append(f'• {vp.display_name} — {vp.referral_count} реф.')
|
||||
else:
|
||||
lines.append('Пока нет виртуальных участников.')
|
||||
|
||||
rows = [
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text='➕ Добавить',
|
||||
callback_data=f'admin_contest_vp_add_{contest_id}',
|
||||
),
|
||||
],
|
||||
]
|
||||
if vps:
|
||||
for vp in vps:
|
||||
rows.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=f'✏️ {vp.display_name}',
|
||||
callback_data=f'admin_contest_vp_edit_{vp.id}',
|
||||
),
|
||||
types.InlineKeyboardButton(
|
||||
text='🗑',
|
||||
callback_data=f'admin_contest_vp_del_{vp.id}',
|
||||
),
|
||||
]
|
||||
)
|
||||
rows.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text='⬅️ Назад',
|
||||
callback_data=f'admin_contest_view_{contest_id}',
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
await callback.message.edit_text(
|
||||
'\n'.join(lines),
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=rows),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_add_virtual_participant(
|
||||
callback: types.CallbackQuery,
|
||||
db_user,
|
||||
db: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
contest_id = int(callback.data.split('_')[-1])
|
||||
await state.set_state(AdminStates.adding_virtual_participant_name)
|
||||
await state.update_data(vp_contest_id=contest_id)
|
||||
await callback.message.edit_text(
|
||||
'👻 Введите отображаемое имя виртуального участника:',
|
||||
reply_markup=types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text='❌ Отмена', callback_data=f'admin_contest_vp_{contest_id}')],
|
||||
]
|
||||
),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_virtual_participant_name(
|
||||
message: types.Message,
|
||||
db_user,
|
||||
db: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
name = message.text.strip()
|
||||
if not name or len(name) > 200:
|
||||
await message.answer('Имя должно быть от 1 до 200 символов. Попробуйте ещё раз:')
|
||||
return
|
||||
await state.update_data(vp_name=name)
|
||||
await state.set_state(AdminStates.adding_virtual_participant_count)
|
||||
await message.answer(f'Имя: <b>{name}</b>\n\nВведите количество рефералов (число):')
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_virtual_participant_count(
|
||||
message: types.Message,
|
||||
db_user,
|
||||
db: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
try:
|
||||
count = int(message.text.strip())
|
||||
if count < 1:
|
||||
raise ValueError
|
||||
except (ValueError, TypeError):
|
||||
await message.answer('Введите положительное целое число:')
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
contest_id = data['vp_contest_id']
|
||||
display_name = data['vp_name']
|
||||
await state.clear()
|
||||
|
||||
vp = await add_virtual_participant(db, contest_id, display_name, count)
|
||||
await message.answer(
|
||||
f'✅ Виртуальный участник добавлен:\nИмя: <b>{vp.display_name}</b>\nРефералов: <b>{vp.referral_count}</b>',
|
||||
reply_markup=types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text='👻 К списку', callback_data=f'admin_contest_vp_{contest_id}')],
|
||||
[types.InlineKeyboardButton(text='⬅️ К конкурсу', callback_data=f'admin_contest_view_{contest_id}')],
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def delete_virtual_participant_handler(
|
||||
callback: types.CallbackQuery,
|
||||
db_user,
|
||||
db: AsyncSession,
|
||||
):
|
||||
vp_id = int(callback.data.split('_')[-1])
|
||||
|
||||
# Получим contest_id до удаления
|
||||
from sqlalchemy import select as sa_select
|
||||
|
||||
from app.database.models import ReferralContestVirtualParticipant
|
||||
|
||||
result = await db.execute(
|
||||
sa_select(ReferralContestVirtualParticipant).where(ReferralContestVirtualParticipant.id == vp_id)
|
||||
)
|
||||
vp = result.scalar_one_or_none()
|
||||
if not vp:
|
||||
await callback.answer('Участник не найден.', show_alert=True)
|
||||
return
|
||||
|
||||
contest_id = vp.contest_id
|
||||
deleted = await delete_virtual_participant(db, vp_id)
|
||||
if deleted:
|
||||
await callback.answer('✅ Удалён', show_alert=False)
|
||||
else:
|
||||
await callback.answer('Не удалось удалить.', show_alert=True)
|
||||
|
||||
# Вернуться к списку
|
||||
vps = await list_virtual_participants(db, contest_id)
|
||||
contest = await get_referral_contest(db, contest_id)
|
||||
|
||||
lines = [f'👻 <b>Виртуальные участники</b> — {contest.title}', '']
|
||||
if vps:
|
||||
for v in vps:
|
||||
lines.append(f'• {v.display_name} — {v.referral_count} реф.')
|
||||
else:
|
||||
lines.append('Пока нет виртуальных участников.')
|
||||
|
||||
rows = [
|
||||
[types.InlineKeyboardButton(text='➕ Добавить', callback_data=f'admin_contest_vp_add_{contest_id}')],
|
||||
]
|
||||
if vps:
|
||||
for v in vps:
|
||||
rows.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=f'✏️ {v.display_name}', callback_data=f'admin_contest_vp_edit_{v.id}'
|
||||
),
|
||||
types.InlineKeyboardButton(text='🗑', callback_data=f'admin_contest_vp_del_{v.id}'),
|
||||
]
|
||||
)
|
||||
rows.append([types.InlineKeyboardButton(text='⬅️ Назад', callback_data=f'admin_contest_view_{contest_id}')])
|
||||
|
||||
await callback.message.edit_text(
|
||||
'\n'.join(lines),
|
||||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=rows),
|
||||
)
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def start_edit_virtual_participant(
|
||||
callback: types.CallbackQuery,
|
||||
db_user,
|
||||
db: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
vp_id = int(callback.data.split('_')[-1])
|
||||
|
||||
from sqlalchemy import select as sa_select
|
||||
|
||||
from app.database.models import ReferralContestVirtualParticipant
|
||||
|
||||
result = await db.execute(
|
||||
sa_select(ReferralContestVirtualParticipant).where(ReferralContestVirtualParticipant.id == vp_id)
|
||||
)
|
||||
vp = result.scalar_one_or_none()
|
||||
if not vp:
|
||||
await callback.answer('Участник не найден.', show_alert=True)
|
||||
return
|
||||
|
||||
await state.set_state(AdminStates.editing_virtual_participant_count)
|
||||
await state.update_data(vp_edit_id=vp_id, vp_edit_contest_id=vp.contest_id)
|
||||
await callback.message.edit_text(
|
||||
f'✏️ <b>{vp.display_name}</b>\n'
|
||||
f'Текущее кол-во рефералов: <b>{vp.referral_count}</b>\n\n'
|
||||
f'Введите новое количество:',
|
||||
reply_markup=types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text='❌ Отмена', callback_data=f'admin_contest_vp_{vp.contest_id}')],
|
||||
]
|
||||
),
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def process_edit_virtual_participant_count(
|
||||
message: types.Message,
|
||||
db_user,
|
||||
db: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
try:
|
||||
count = int(message.text.strip())
|
||||
if count < 1:
|
||||
raise ValueError
|
||||
except (ValueError, TypeError):
|
||||
await message.answer('Введите положительное целое число:')
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
vp_id = data['vp_edit_id']
|
||||
contest_id = data['vp_edit_contest_id']
|
||||
await state.clear()
|
||||
|
||||
vp = await update_virtual_participant_count(db, vp_id, count)
|
||||
if vp:
|
||||
await message.answer(
|
||||
f'✅ Обновлено: <b>{vp.display_name}</b> — {vp.referral_count} реф.',
|
||||
reply_markup=types.InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[types.InlineKeyboardButton(text='👻 К списку', callback_data=f'admin_contest_vp_{contest_id}')],
|
||||
]
|
||||
),
|
||||
)
|
||||
else:
|
||||
await message.answer('Участник не найден.')
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher):
|
||||
dp.callback_query.register(show_contests_menu, F.data == 'admin_contests')
|
||||
dp.callback_query.register(show_referral_contests_menu, F.data == 'admin_contests_referral')
|
||||
@@ -996,3 +1278,11 @@ def register_handlers(dp: Dispatcher):
|
||||
dp.message.register(process_end_date, AdminStates.creating_referral_contest_end)
|
||||
dp.message.register(finalize_contest_creation, AdminStates.creating_referral_contest_time)
|
||||
dp.message.register(process_edit_summary_times, AdminStates.editing_referral_contest_summary_times)
|
||||
|
||||
dp.callback_query.register(start_add_virtual_participant, F.data.startswith('admin_contest_vp_add_'))
|
||||
dp.callback_query.register(delete_virtual_participant_handler, F.data.startswith('admin_contest_vp_del_'))
|
||||
dp.callback_query.register(start_edit_virtual_participant, F.data.startswith('admin_contest_vp_edit_'))
|
||||
dp.callback_query.register(show_virtual_participants, F.data.regexp(r'^admin_contest_vp_\d+$'))
|
||||
dp.message.register(process_virtual_participant_name, AdminStates.adding_virtual_participant_name)
|
||||
dp.message.register(process_virtual_participant_count, AdminStates.adding_virtual_participant_count)
|
||||
dp.message.register(process_edit_virtual_participant_count, AdminStates.editing_virtual_participant_count)
|
||||
|
||||
@@ -649,6 +649,12 @@ def get_referral_contest_manage_keyboard(
|
||||
callback_data=f'admin_contest_edit_times_{contest_id}',
|
||||
),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text='👻 Виртуальные',
|
||||
callback_data=f'admin_contest_vp_{contest_id}',
|
||||
),
|
||||
],
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text='🔄 Синхронизация',
|
||||
|
||||
@@ -22,6 +22,7 @@ from app.database.models import (
|
||||
CryptoBotPayment,
|
||||
FreekassaPayment,
|
||||
HeleketPayment,
|
||||
KassaAiPayment,
|
||||
MulenPayPayment,
|
||||
Pal24Payment,
|
||||
PaymentMethod,
|
||||
@@ -109,6 +110,8 @@ def method_display_name(method: PaymentMethod) -> str:
|
||||
return 'CloudPayments'
|
||||
if method == PaymentMethod.FREEKASSA:
|
||||
return 'Freekassa'
|
||||
if method == PaymentMethod.KASSA_AI:
|
||||
return settings.get_kassa_ai_display_name()
|
||||
if method == PaymentMethod.TELEGRAM_STARS:
|
||||
return 'Telegram Stars'
|
||||
return method.value
|
||||
@@ -133,6 +136,8 @@ def _method_is_enabled(method: PaymentMethod) -> bool:
|
||||
return settings.is_cloudpayments_enabled()
|
||||
if method == PaymentMethod.FREEKASSA:
|
||||
return settings.is_freekassa_enabled()
|
||||
if method == PaymentMethod.KASSA_AI:
|
||||
return settings.is_kassa_ai_enabled()
|
||||
return False
|
||||
|
||||
|
||||
@@ -356,6 +361,13 @@ def _is_freekassa_pending(payment: FreekassaPayment) -> bool:
|
||||
return status in {'pending', 'created', 'processing'}
|
||||
|
||||
|
||||
def _is_kassa_ai_pending(payment: KassaAiPayment) -> bool:
|
||||
if payment.is_paid:
|
||||
return False
|
||||
status = (payment.status or '').lower()
|
||||
return status in {'pending', 'created', 'processing'}
|
||||
|
||||
|
||||
def _parse_cryptobot_amount_kopeks(payment: CryptoBotPayment) -> int:
|
||||
payload = payment.payload or ''
|
||||
match = re.search(r'_(\d+)$', payload)
|
||||
@@ -648,6 +660,31 @@ async def _fetch_freekassa_payments(db: AsyncSession, cutoff: datetime) -> list[
|
||||
return records
|
||||
|
||||
|
||||
async def _fetch_kassa_ai_payments(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||||
stmt = (
|
||||
select(KassaAiPayment)
|
||||
.options(selectinload(KassaAiPayment.user))
|
||||
.where(KassaAiPayment.created_at >= cutoff)
|
||||
.order_by(desc(KassaAiPayment.created_at))
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
records: list[PendingPayment] = []
|
||||
for payment in result.scalars().all():
|
||||
if not _is_kassa_ai_pending(payment):
|
||||
continue
|
||||
record = _build_record(
|
||||
PaymentMethod.KASSA_AI,
|
||||
payment,
|
||||
identifier=payment.order_id,
|
||||
amount_kopeks=payment.amount_kopeks,
|
||||
status=payment.status or '',
|
||||
is_paid=bool(payment.is_paid),
|
||||
)
|
||||
if record:
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
|
||||
async def _fetch_stars_transactions(db: AsyncSession, cutoff: datetime) -> list[PendingPayment]:
|
||||
stmt = (
|
||||
select(Transaction)
|
||||
@@ -694,6 +731,7 @@ async def list_recent_pending_payments(
|
||||
await _fetch_cryptobot_payments(db, cutoff),
|
||||
await _fetch_cloudpayments_payments(db, cutoff),
|
||||
await _fetch_freekassa_payments(db, cutoff),
|
||||
await _fetch_kassa_ai_payments(db, cutoff),
|
||||
await _fetch_stars_transactions(db, cutoff),
|
||||
)
|
||||
|
||||
@@ -848,6 +886,20 @@ async def get_payment_record(
|
||||
is_paid=bool(payment.is_paid),
|
||||
)
|
||||
|
||||
if method == PaymentMethod.KASSA_AI:
|
||||
payment = await db.get(KassaAiPayment, local_payment_id)
|
||||
if not payment:
|
||||
return None
|
||||
await db.refresh(payment, attribute_names=['user'])
|
||||
return _build_record(
|
||||
method,
|
||||
payment,
|
||||
identifier=payment.order_id,
|
||||
amount_kopeks=payment.amount_kopeks,
|
||||
status=payment.status or '',
|
||||
is_paid=bool(payment.is_paid),
|
||||
)
|
||||
|
||||
if method == PaymentMethod.TELEGRAM_STARS:
|
||||
transaction = await db.get(Transaction, local_payment_id)
|
||||
if not transaction:
|
||||
|
||||
@@ -12,10 +12,11 @@ from app.config import settings
|
||||
from app.database.crud.referral_contest import (
|
||||
add_contest_event,
|
||||
get_contest_events_count,
|
||||
get_contest_leaderboard,
|
||||
get_contest_leaderboard_with_virtual,
|
||||
get_contests_for_events,
|
||||
get_contests_for_summaries,
|
||||
get_referrer_score,
|
||||
list_virtual_participants,
|
||||
mark_daily_summary_sent,
|
||||
mark_final_summary_sent,
|
||||
)
|
||||
@@ -173,8 +174,10 @@ class ReferralContestService:
|
||||
day_start_utc = day_start_local.astimezone(UTC).replace(tzinfo=None)
|
||||
day_end_utc = day_end_local.astimezone(UTC).replace(tzinfo=None)
|
||||
|
||||
leaderboard = list(await get_contest_leaderboard(db, contest.id))
|
||||
total_events = await get_contest_events_count(db, contest.id)
|
||||
leaderboard = await get_contest_leaderboard_with_virtual(db, contest.id)
|
||||
virtual_participants = await list_virtual_participants(db, contest.id)
|
||||
virtual_count = sum(vp.referral_count for vp in virtual_participants)
|
||||
total_events = await get_contest_events_count(db, contest.id) + virtual_count
|
||||
today_events = await get_contest_events_count(
|
||||
db,
|
||||
contest.id,
|
||||
@@ -269,7 +272,7 @@ class ReferralContestService:
|
||||
self,
|
||||
*,
|
||||
contest: ReferralContest,
|
||||
leaderboard: Sequence[tuple[User, int, int]],
|
||||
leaderboard: Sequence[tuple[str, int, int, bool]],
|
||||
total_events: int,
|
||||
today_events: int,
|
||||
is_final: bool,
|
||||
@@ -293,10 +296,9 @@ class ReferralContestService:
|
||||
]
|
||||
|
||||
if leaderboard:
|
||||
for idx, (user, score, _) in enumerate(leaderboard[:5], start=1):
|
||||
name = user.full_name
|
||||
user_id_display = user.telegram_id or user.email or f'#{user.id}'
|
||||
lines.append(f'{idx}. {name} ({user_id_display}) — {score}')
|
||||
for idx, (name, score, _, is_virtual) in enumerate(leaderboard[:5], start=1):
|
||||
virt_mark = ' 👻' if is_virtual else ''
|
||||
lines.append(f'{idx}. {name}{virt_mark} — {score}')
|
||||
else:
|
||||
lines.append('Пока нет участников.')
|
||||
|
||||
@@ -318,7 +320,7 @@ class ReferralContestService:
|
||||
self,
|
||||
*,
|
||||
contest: ReferralContest,
|
||||
leaderboard: Sequence[tuple[User, int, int]],
|
||||
leaderboard: Sequence[tuple[str, int, int, bool]],
|
||||
total_events: int,
|
||||
today_events: int,
|
||||
is_final: bool,
|
||||
@@ -346,8 +348,8 @@ class ReferralContestService:
|
||||
]
|
||||
|
||||
if leaderboard:
|
||||
for idx, (user, score, _) in enumerate(leaderboard[:5], start=1):
|
||||
lines.append(f'{idx}. {user.full_name} — {score}')
|
||||
for idx, (name, score, _, _is_virtual) in enumerate(leaderboard[:5], start=1):
|
||||
lines.append(f'{idx}. {name} — {score}')
|
||||
else:
|
||||
lines.append('Пока нет участников.')
|
||||
|
||||
@@ -537,8 +539,12 @@ class ReferralContestService:
|
||||
for contest in contests:
|
||||
try:
|
||||
# Проверяем что реферал зарегистрировался В ПЕРИОД конкурса
|
||||
user_created_at = user.created_at if user.created_at.tzinfo is None else user.created_at.replace(tzinfo=None)
|
||||
contest_start = contest.start_at if contest.start_at.tzinfo is None else contest.start_at.replace(tzinfo=None)
|
||||
user_created_at = (
|
||||
user.created_at if user.created_at.tzinfo is None else user.created_at.replace(tzinfo=None)
|
||||
)
|
||||
contest_start = (
|
||||
contest.start_at if contest.start_at.tzinfo is None else contest.start_at.replace(tzinfo=None)
|
||||
)
|
||||
contest_end = contest.end_at if contest.end_at.tzinfo is None else contest.end_at.replace(tzinfo=None)
|
||||
|
||||
if user_created_at < contest_start or user_created_at > contest_end:
|
||||
@@ -594,8 +600,12 @@ class ReferralContestService:
|
||||
for contest in contests:
|
||||
try:
|
||||
# Проверяем что реферал зарегистрировался В ПЕРИОД конкурса
|
||||
user_created_at = user.created_at if user.created_at.tzinfo is None else user.created_at.replace(tzinfo=None)
|
||||
contest_start = contest.start_at if contest.start_at.tzinfo is None else contest.start_at.replace(tzinfo=None)
|
||||
user_created_at = (
|
||||
user.created_at if user.created_at.tzinfo is None else user.created_at.replace(tzinfo=None)
|
||||
)
|
||||
contest_start = (
|
||||
contest.start_at if contest.start_at.tzinfo is None else contest.start_at.replace(tzinfo=None)
|
||||
)
|
||||
contest_end = contest.end_at if contest.end_at.tzinfo is None else contest.end_at.replace(tzinfo=None)
|
||||
|
||||
if user_created_at < contest_start or user_created_at > contest_end:
|
||||
|
||||
@@ -115,6 +115,9 @@ class AdminStates(StatesGroup):
|
||||
creating_referral_contest_end = State()
|
||||
creating_referral_contest_time = State()
|
||||
editing_referral_contest_summary_times = State()
|
||||
adding_virtual_participant_name = State()
|
||||
adding_virtual_participant_count = State()
|
||||
editing_virtual_participant_count = State()
|
||||
editing_daily_contest_field = State()
|
||||
editing_daily_contest_value = State()
|
||||
|
||||
|
||||
@@ -256,7 +256,11 @@ async def compute_simple_subscription_price(
|
||||
)
|
||||
|
||||
total_before_discount = (
|
||||
base_price_original + traffic_price_original + devices_price_original + servers_price_original + modem_price_original
|
||||
base_price_original
|
||||
+ traffic_price_original
|
||||
+ devices_price_original
|
||||
+ servers_price_original
|
||||
+ modem_price_original
|
||||
)
|
||||
|
||||
total_discount = base_discount + traffic_discount + devices_discount + servers_discount_total
|
||||
|
||||
@@ -2,20 +2,18 @@
|
||||
Упрощенные тесты для проверки логики уведомлений Kassa AI.
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
def test_notification_message_bright_prompt():
|
||||
"""
|
||||
Тест: проверяем что формируется ЯРКОЕ сообщение с SHOW_ACTIVATION_PROMPT_AFTER_TOPUP=true.
|
||||
"""
|
||||
from app.config import settings
|
||||
|
||||
# Эмулируем код из kassa_ai.py
|
||||
SHOW_ACTIVATION_PROMPT_AFTER_TOPUP = True
|
||||
display_name = "Kassa AI"
|
||||
amount_formatted = "10₽"
|
||||
display_name = 'Kassa AI'
|
||||
amount_formatted = '10₽'
|
||||
|
||||
if SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
|
||||
message = (
|
||||
@@ -37,7 +35,7 @@ def test_notification_message_bright_prompt():
|
||||
assert '👇' in message
|
||||
assert display_name in message
|
||||
assert amount_formatted in message
|
||||
print(f"\n✅ ЯРКОЕ сообщение сформировано правильно:\n{message}")
|
||||
print(f'\n✅ ЯРКОЕ сообщение сформировано правильно:\n{message}')
|
||||
|
||||
|
||||
def test_notification_message_standard():
|
||||
@@ -46,8 +44,8 @@ def test_notification_message_standard():
|
||||
"""
|
||||
# Эмулируем код из kassa_ai.py
|
||||
SHOW_ACTIVATION_PROMPT_AFTER_TOPUP = False
|
||||
display_name = "Kassa AI"
|
||||
amount_formatted = "10₽"
|
||||
display_name = 'Kassa AI'
|
||||
amount_formatted = '10₽'
|
||||
|
||||
if SHOW_ACTIVATION_PROMPT_AFTER_TOPUP:
|
||||
message = ''
|
||||
@@ -69,7 +67,7 @@ def test_notification_message_standard():
|
||||
assert 'Платеж успешно завершен' in message
|
||||
assert display_name in message
|
||||
assert amount_formatted in message
|
||||
print(f"\n✅ Обычное сообщение сформировано правильно:\n{message}")
|
||||
print(f'\n✅ Обычное сообщение сформировано правильно:\n{message}')
|
||||
|
||||
|
||||
def test_telegram_id_saved_before_commit():
|
||||
@@ -92,7 +90,7 @@ def test_telegram_id_saved_before_commit():
|
||||
# Проверяем что локальные переменные сохранились
|
||||
assert user_telegram_id == 123456789
|
||||
assert user_language == 'ru'
|
||||
print(f"\n✅ telegram_id сохранен в локальную переменную: {user_telegram_id}")
|
||||
print(f'\n✅ telegram_id сохранен в локальную переменную: {user_telegram_id}')
|
||||
|
||||
|
||||
def test_send_message_called_with_correct_params():
|
||||
@@ -103,7 +101,7 @@ def test_send_message_called_with_correct_params():
|
||||
bot.send_message = MagicMock()
|
||||
|
||||
user_telegram_id = 123456789
|
||||
message = "Тестовое сообщение"
|
||||
message = 'Тестовое сообщение'
|
||||
keyboard = MagicMock()
|
||||
|
||||
# Эмулируем вызов
|
||||
@@ -121,7 +119,7 @@ def test_send_message_called_with_correct_params():
|
||||
assert call_args[1]['chat_id'] == 123456789
|
||||
assert call_args[1]['parse_mode'] == 'HTML'
|
||||
assert call_args[1]['text'] == message
|
||||
print(f"\n✅ bot.send_message вызван с правильными параметрами")
|
||||
print('\n✅ bot.send_message вызван с правильными параметрами')
|
||||
|
||||
|
||||
def test_no_send_when_no_telegram_id():
|
||||
@@ -135,8 +133,8 @@ def test_no_send_when_no_telegram_id():
|
||||
|
||||
# Эмулируем проверку
|
||||
if bot and user_telegram_id:
|
||||
bot.send_message(chat_id=user_telegram_id, text="test")
|
||||
bot.send_message(chat_id=user_telegram_id, text='test')
|
||||
|
||||
# Проверка
|
||||
bot.send_message.assert_not_called()
|
||||
print(f"\n✅ bot.send_message НЕ вызван когда telegram_id=None")
|
||||
print('\n✅ bot.send_message НЕ вызван когда telegram_id=None')
|
||||
|
||||
Reference in New Issue
Block a user