Require club cards in cashier enrollment and restore ticket owners
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -36,7 +36,7 @@
|
||||
|
||||
- «Участники» → «Добавить»: выберите розыгрыш и введите Telegram ID или username согласно подсказке.
|
||||
- «Участники» → «Массовые операции» → «Добавление по счетам»: выберите розыгрыш и передайте номера в формате `11-22-33-44-55-66-77`.
|
||||
- Кассир может добавлять счета через `/cashier`; все сотрудники работают с общей базой, каждый ведёт собственный диалог ввода.
|
||||
- Кассир добавляет счета через `/cashier` только в формате `КАРТА СЧЁТ`; карта обязательна в каждой строке. Все сотрудники работают с общей базой, каждый ведёт собственный диалог ввода.
|
||||
- Перед проведением проверьте «По розыгрышам» / «Отчёт»: состав, количество участников, отсутствие ошибочно добавленных счетов.
|
||||
- Ошибочное участие удаляйте через «Удалить» либо соответствующую массовую операцию до проведения розыгрыша. Повторное добавление того же счёта в один розыгрыш не создаёт второй билет.
|
||||
|
||||
@@ -44,6 +44,8 @@
|
||||
|
||||
Бот поддерживает участие по пользователю и по счёту. Используйте способ, соответствующий опубликованным правилам. Счёт без указания карты может участвовать без связанного профиля; в этом случае автоматическое личное уведомление владельцу невозможно до установления связи.
|
||||
|
||||
Для восстановления связи ранее добавленного счёта в открытом розыгрыше отправьте его повторно с правильной картой. Бот привяжет существующее участие без создания второго билета. Перепривязка участия другого владельца отклоняется. Через кассу добавление без карты запрещено.
|
||||
|
||||
## Проведение и выдача призов
|
||||
|
||||
1. Проверьте призы, состав участников и выбранный розыгрыш. Если использовались ручные назначения победителей, проверьте их до запуска: они учитываются при проведении.
|
||||
|
||||
@@ -28,21 +28,23 @@
|
||||
## Добавление счетов в розыгрыш
|
||||
|
||||
1. `/cashier` → выберите нужное название открытого розыгрыша.
|
||||
2. Отправьте счета, каждый с новой строки:
|
||||
2. Отправьте клубную карту и счёт клиента, по одной паре на строку. Карта обязательна для каждого счёта, включая уже известные боту:
|
||||
|
||||
```text
|
||||
11-22-33-44-55-66-77
|
||||
88-99-00-11-22-33-44
|
||||
0007 11-22-33-44-55-66-77
|
||||
0008 88-99-00-11-22-33-44
|
||||
```
|
||||
|
||||
3. За один ввод допускается до 1000 распознанных счетов.
|
||||
4. Проверьте количества «Добавлено», «Пропущено» и ошибки. Повтор того же счёта в том же розыгрыше не создаёт второго участия.
|
||||
4. Проверьте количества «Добавлено», «Привязано существующих участий», «Пропущено» и ошибки. Повтор того же счёта в том же розыгрыше не создаёт второго участия.
|
||||
|
||||
Чтобы сразу привязать новый счёт клиенту, отправьте строку `КАРТА СЧЁТ`, например `0007 11-22-33-44-55-66-77`. Карта должна быть известна боту. Создание счёта и добавление участия сохраняются вместе. Чужой или неактивный счёт нельзя перепривязать таким вводом; исправьте данные или обратитесь к администратору.
|
||||
Карта должна быть известна боту. Создание нового счёта и добавление участия сохраняются вместе. Ведущие нули карты сохраняются. Если хотя бы одна строка не содержит карты, весь список возвращается на исправление без добавления его строк; отправьте исправленный список в том же диалоге.
|
||||
|
||||
Чужой или неактивный счёт нельзя перепривязать таким вводом; исправьте данные или обратитесь к администратору.
|
||||
|
||||
Если розыгрыш уже проведён или закрыт, бот отклонит добавление. Если активных розыгрышей нет, обратитесь к администратору. Кассир не может открыть или создать розыгрыш.
|
||||
|
||||
Счёт без привязки к клиенту тоже может быть добавлен. Чтобы клиент получил личное уведомление и мог подтвердить выигрыш по коду, сначала проверьте привязку счёта к его зарегистрированному профилю.
|
||||
Если счёт раньше попал в открытый розыгрыш без клиента, повторите ввод с правильной картой. Бот восстановит связь существующего участия с клиентом, сохранив номер билета и время добавления. Это позволит клиенту видеть счёт, получать уведомление о выигрыше и подтверждать выдачу своим кодом. Владелец уже привязанного участия не заменяется.
|
||||
|
||||
## Исправление счёта
|
||||
|
||||
|
||||
@@ -251,6 +251,8 @@ async def add_accounts_to_lottery(callback: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
text = f"Результаты добавления в розыгрыш:\n{lottery_title}\n\n"
|
||||
text += f"✅ Добавлено: {results['added']}\n"
|
||||
if results['linked']:
|
||||
text += f"✅ Привязано существующих участий: {results['linked']}\n"
|
||||
text += f"⚠️ Пропущено: {results['skipped']}\n\n"
|
||||
|
||||
if results['details']:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Сервис для работы с участием счетов в розыгрышах (без привязки к пользователям)
|
||||
Сервис участия счетов: привязка к клиентам и проверки открытого розыгрыша.
|
||||
"""
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, delete, func
|
||||
@@ -7,6 +7,7 @@ from ..core.models import Account, Lottery, Participation, User, Winner
|
||||
from ..utils.account_utils import validate_account_number, format_account_number, parse_accounts_from_message, search_accounts_by_pattern
|
||||
from typing import List, Optional, Dict, Any
|
||||
from ..core.transactions import lock_open_lottery
|
||||
from ..utils.account_input import card_account_entry
|
||||
|
||||
|
||||
class AccountParticipationService:
|
||||
@@ -16,15 +17,22 @@ class AccountParticipationService:
|
||||
async def add_account_to_lottery(
|
||||
session: AsyncSession,
|
||||
lottery_id: int,
|
||||
account_number: str
|
||||
account_number: str,
|
||||
*, require_card: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Добавить счет в розыгрыш.
|
||||
Поддерживает форматы: "КАРТА СЧЕТ" или просто "СЧЕТ"
|
||||
В кассе require_card=True запрещает участие без явно указанной карты.
|
||||
|
||||
Returns:
|
||||
Dict с ключами: success, message, account_number
|
||||
"""
|
||||
if require_card:
|
||||
try:
|
||||
account_number = card_account_entry(account_number)
|
||||
except ValueError as error:
|
||||
return {"success": False, "message": str(error), "account_number": account_number}
|
||||
# Разделяем по пробелу если есть номер карты
|
||||
parts = account_number.split()
|
||||
if len(parts) == 2:
|
||||
@@ -69,7 +77,8 @@ class AccountParticipationService:
|
||||
Participation.account_number == formatted_account
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
existing = existing.scalar_one_or_none()
|
||||
if existing and not card_number:
|
||||
await session.rollback()
|
||||
card_info = f" (карта: {card_number})" if card_number else ""
|
||||
return {
|
||||
@@ -100,6 +109,21 @@ class AccountParticipationService:
|
||||
if card_number and (not user or user.club_card_number != card_number):
|
||||
await session.rollback()
|
||||
return {"success": False, "message": "Карта не соответствует владельцу счета", "account_number": formatted_account}
|
||||
|
||||
if existing:
|
||||
if user and (existing.user_id not in (None, user.id) or existing.account_id not in (None, account_record.id)):
|
||||
await session.rollback()
|
||||
return {"success": False, "message": "Участие связано с другим владельцем. Обратитесь к администратору.",
|
||||
"account_number": formatted_account}
|
||||
if user and (existing.user_id is None or existing.account_id is None):
|
||||
existing.user_id = user.id
|
||||
existing.account_id = account_record.id
|
||||
await session.commit()
|
||||
return {"success": True, "linked": True, "account_number": formatted_account,
|
||||
"message": f"Счет {formatted_account} привязан к карте {card_number}; новое участие не создавалось"}
|
||||
await session.rollback()
|
||||
return {"success": False, "message": f"Счет {formatted_account} уже участвует в розыгрыше",
|
||||
"account_number": formatted_account}
|
||||
|
||||
# Добавляем участие с полными данными
|
||||
participation = Participation(
|
||||
@@ -122,26 +146,40 @@ class AccountParticipationService:
|
||||
async def add_accounts_bulk(
|
||||
session: AsyncSession,
|
||||
lottery_id: int,
|
||||
account_numbers: List[str]
|
||||
account_numbers: List[str],
|
||||
*, require_card: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Массовое добавление счетов в розыгрыш
|
||||
"""
|
||||
results = {
|
||||
"added": 0,
|
||||
"linked": 0,
|
||||
"skipped": 0,
|
||||
"errors": [],
|
||||
"details": [],
|
||||
"added_accounts": [],
|
||||
"skipped_accounts": []
|
||||
}
|
||||
|
||||
if require_card:
|
||||
try:
|
||||
account_numbers = [card_account_entry(account) for account in account_numbers]
|
||||
except ValueError as error:
|
||||
results["skipped"] = len(account_numbers)
|
||||
results["skipped_accounts"] = list(account_numbers)
|
||||
results["errors"].append(str(error))
|
||||
return results
|
||||
|
||||
for account in account_numbers:
|
||||
result = await AccountParticipationService.add_account_to_lottery(
|
||||
session, lottery_id, account
|
||||
session, lottery_id, account, require_card=require_card
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
if result.get("linked"):
|
||||
results["linked"] += 1
|
||||
results["details"].append(f"✅ {result['message']}")
|
||||
elif result["success"]:
|
||||
results["added"] += 1
|
||||
results["added_accounts"].append(result["account_number"])
|
||||
results["details"].append(f"✅ {result['account_number']}")
|
||||
|
||||
@@ -359,7 +359,8 @@ async def add_accounts_to_lottery(callback: CallbackQuery, state: FSMContext):
|
||||
|
||||
lottery_title = lottery.title
|
||||
from .account_services import AccountParticipationService
|
||||
result = await AccountParticipationService.add_accounts_bulk(session, lottery_id, [a['account_number'] for a in accounts])
|
||||
entries = [f"{a.get('club_card') or ''} {a['account_number']}".strip() for a in accounts]
|
||||
result = await AccountParticipationService.add_accounts_bulk(session, lottery_id, entries, require_card=True)
|
||||
success_count = result['added']
|
||||
errors = result['errors']
|
||||
|
||||
@@ -368,6 +369,8 @@ async def add_accounts_to_lottery(callback: CallbackQuery, state: FSMContext):
|
||||
|
||||
if success_count:
|
||||
text += f"✅ Добавлено счетов: {success_count}\n\n"
|
||||
if result['linked']:
|
||||
text += f"✅ Привязано существующих участий: {result['linked']}\n\n"
|
||||
|
||||
if errors:
|
||||
text += f"⚠️ Ошибки: {len(errors)}\n"
|
||||
|
||||
@@ -2056,6 +2056,8 @@ async def process_bulk_add_accounts(message: Message, state: FSMContext):
|
||||
text = f"🏦 Результат массового добавления по счетам\n\n"
|
||||
text += f"🎯 Розыгрыш: {lottery.title}\n\n"
|
||||
text += f"✅ Добавлено: {results['added']}\n"
|
||||
if results['linked']:
|
||||
text += f"✅ Привязано существующих участий: {results['linked']}\n"
|
||||
text += f"⚠️ Уже участвуют: {results['skipped']}\n"
|
||||
text += f"🚫 Неверных форматов: {len(results['invalid_accounts'])}\n"
|
||||
text += f"❌ Ошибок: {len(results['errors'])}\n\n"
|
||||
|
||||
@@ -12,7 +12,7 @@ from src.core.database import async_session_maker
|
||||
from src.core.services import LotteryService
|
||||
from src.handlers.account_services import AccountParticipationService
|
||||
from src.middlewares.access import AccessMiddleware
|
||||
from src.utils.account_input import parse_account_records
|
||||
from src.utils.account_input import parse_cashier_records
|
||||
|
||||
cashier_router = Router(name="cashier")
|
||||
cashier_router.message.middleware(AccessMiddleware(allow_cashier=True))
|
||||
@@ -33,7 +33,7 @@ async def cashier_menu(message: Message, state: FSMContext):
|
||||
for lottery in lotteries]
|
||||
buttons.append([InlineKeyboardButton(text="📖 Инструкция кассира", callback_data="guide:cashier")])
|
||||
await message.answer(
|
||||
"💼 Касса\n\nВыберите розыгрыш для добавления участников по счетам.\n\n"
|
||||
"💼 Касса\n\nВыберите розыгрыш. Для каждого счёта обязательна клубная карта клиента.\n\n"
|
||||
"/add_account КАРТА СЧЕТ — привязать счет клиенту\n"
|
||||
"/remove_account СЧЕТ — деактивировать счет\n"
|
||||
"/verify_winner КОД ID_РОЗЫГРЫША — подтвердить выдачу приза\n"
|
||||
@@ -62,21 +62,27 @@ async def choose_draw(callback: CallbackQuery, state: FSMContext):
|
||||
await state.update_data(lottery_id=lottery_id)
|
||||
await state.set_state(CashierStates.accounts)
|
||||
await callback.answer()
|
||||
await callback.message.answer("Отправьте счета, каждый с новой строки, или /cancel.")
|
||||
await callback.message.answer(
|
||||
"Отправьте клубную карту и счёт клиента, по одной паре на строку:\n"
|
||||
"0007 11-22-33-44-55-66-77\n"
|
||||
"0008 88-99-00-11-22-33-44\n\n"
|
||||
"Карта обязательна для каждого счёта. /cancel — отмена."
|
||||
)
|
||||
|
||||
|
||||
@cashier_router.message(CashierStates.accounts, F.text, NotCommand())
|
||||
async def add_accounts(message: Message, state: FSMContext):
|
||||
accounts, errors = parse_account_records(message.text)
|
||||
accounts, errors = parse_cashier_records(message.text)
|
||||
if errors:
|
||||
await message.answer("Исправьте записи и отправьте список заново:\n" + "\n".join(errors[:10]))
|
||||
return
|
||||
if not accounts or len(accounts) > 1000:
|
||||
await message.answer("Введите от 1 до 1000 счетов в формате 11-22-33-44-55-66-77.")
|
||||
await message.answer("Введите от 1 до 1000 строк в формате КАРТА СЧЁТ: 0007 11-22-33-44-55-66-77.")
|
||||
return
|
||||
data = await state.get_data()
|
||||
async with async_session_maker() as session:
|
||||
result = await AccountParticipationService.add_accounts_bulk(session, data["lottery_id"], accounts)
|
||||
result = await AccountParticipationService.add_accounts_bulk(session, data["lottery_id"], accounts, require_card=True)
|
||||
await state.clear()
|
||||
await message.answer(f"Добавлено: {result['added']}. Пропущено: {result['skipped']}.\n"
|
||||
+ (f"Привязано существующих участий: {result['linked']}.\n" if result['linked'] else "")
|
||||
+ "\n".join(result["errors"][:10]))
|
||||
|
||||
@@ -5,6 +5,38 @@ ACCOUNT = re.compile(r"(?<![\w-])(?:[0-9]{2}(?:[- ][0-9]{2}){6}|[0-9]{14})(?![\w
|
||||
CARD = re.compile(r"[0-9]{1,50}")
|
||||
|
||||
|
||||
def card_account_entry(value):
|
||||
"""Validate an explicit card/account pair, including a 14-digit card."""
|
||||
from .account_utils import format_account_number
|
||||
parts = value.strip().split(maxsplit=1)
|
||||
if len(parts) != 2 or not CARD.fullmatch(parts[0]) or not ACCOUNT.fullmatch(parts[1]):
|
||||
raise ValueError("Для каждого счёта обязательна клубная карта. Формат: КАРТА СЧЁТ, например 0007 11-22-33-44-55-66-77.")
|
||||
return f"{parts[0]} {format_account_number(parts[1])}"
|
||||
|
||||
|
||||
def parse_cashier_records(text):
|
||||
# Keep the existing table/Viposnova formats, but never accept an unbound row.
|
||||
entries, errors = [], []
|
||||
if not any(line.strip().lower().startswith("viposnova") for line in text.splitlines()):
|
||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||
try:
|
||||
entries = [card_account_entry(line) for line in lines]
|
||||
except ValueError:
|
||||
entries = []
|
||||
if not entries:
|
||||
entries, errors = parse_account_records(text)
|
||||
checked = []
|
||||
for entry in entries:
|
||||
try:
|
||||
pair = card_account_entry(entry)
|
||||
except ValueError as error:
|
||||
errors.append(str(error))
|
||||
continue
|
||||
if pair not in checked:
|
||||
checked.append(pair)
|
||||
return checked, list(dict.fromkeys(errors))
|
||||
|
||||
|
||||
def parse_account_records(text):
|
||||
from .account_utils import format_account_number
|
||||
lines = [line.strip() for line in text.splitlines() if line.strip()]
|
||||
|
||||
148
tests/test_cashier_cards.py
Normal file
148
tests/test_cashier_cards.py
Normal file
@@ -0,0 +1,148 @@
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from src.core.database import async_session_maker
|
||||
from src.core.models import Account, Participation, User, Winner
|
||||
from src.core.services import LotteryService
|
||||
from src.handlers.account_services import AccountParticipationService
|
||||
from src.handlers.cashier_handlers import CashierStates
|
||||
from src.utils.account_input import parse_cashier_records
|
||||
from test_operator_scenarios import ACCOUNT, ADMIN, CLIENT, assert_clean, output, seed, send
|
||||
from test_text_inputs import context
|
||||
|
||||
NEW_ACCOUNT = "01-02-03-04-05-06-07"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text", [ACCOUNT, NEW_ACCOUNT, f"0007 {NEW_ACCOUNT}\n{ACCOUNT}"])
|
||||
async def test_cashier_requires_card_for_every_row_without_partial_writes(text, caplog):
|
||||
lottery_id, _ = await seed(participants=False)
|
||||
await send(900002, callback=f"cash_add:{lottery_id}")
|
||||
calls = await send(900002, text=text)
|
||||
assert "обязательна клубная карта" in output(calls)
|
||||
assert await context(900002).get_state() == CashierStates.accounts.state
|
||||
async with async_session_maker() as session:
|
||||
assert await session.scalar(select(func.count(Participation.id))) == 0
|
||||
assert await session.scalar(select(func.count(Account.id))) == 2
|
||||
await send(900002, text=f"0007 {NEW_ACCOUNT}")
|
||||
assert await context(900002).get_state() is None
|
||||
async with async_session_maker() as session:
|
||||
assert await session.scalar(select(func.count(Participation.id))) == 1
|
||||
assert_clean(caplog)
|
||||
|
||||
|
||||
async def test_strict_service_rejects_missing_card_before_any_writes():
|
||||
lottery_id, _ = await seed(participants=False)
|
||||
async with async_session_maker() as session:
|
||||
result = await AccountParticipationService.add_account_to_lottery(session, lottery_id, ACCOUNT, require_card=True)
|
||||
assert not result["success"]
|
||||
result = await AccountParticipationService.add_accounts_bulk(
|
||||
session, lottery_id, [f"0007 {NEW_ACCOUNT}", ACCOUNT], require_card=True
|
||||
)
|
||||
assert result["added"] == result["linked"] == 0 and result["errors"]
|
||||
assert await session.scalar(select(func.count(Participation.id))) == 0
|
||||
assert await session.scalar(select(func.count(Account.id))) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("existing_account", [False, True])
|
||||
async def test_cashier_restores_anonymous_participation_and_client_interaction(existing_account, caplog):
|
||||
lottery_id, owner_id = await seed(participants=False)
|
||||
account_number = ACCOUNT if existing_account else NEW_ACCOUNT
|
||||
async with async_session_maker() as session:
|
||||
ticket = Participation(lottery_id=lottery_id, account_number=account_number)
|
||||
session.add(ticket)
|
||||
await session.commit()
|
||||
ticket_id, created_at = ticket.id, ticket.created_at
|
||||
await send(900002, callback=f"cash_add:{lottery_id}")
|
||||
calls = await send(900002, text=f"0007 {account_number}")
|
||||
assert "Привязано существующих участий: 1" in output(calls)
|
||||
assert account_number in output(await send(CLIENT, text="/my_accounts"))
|
||||
async with async_session_maker() as session:
|
||||
ticket = await session.get(Participation, ticket_id)
|
||||
account = await session.scalar(select(Account).where(Account.account_number == account_number))
|
||||
assert ticket.user_id == account.owner_id == owner_id and ticket.account_id == account.id
|
||||
assert ticket.created_at.replace(tzinfo=None) == created_at.replace(tzinfo=None)
|
||||
assert await session.scalar(select(func.count(Participation.id))) == 1
|
||||
await send(900002, callback=f"cash_add:{lottery_id}")
|
||||
calls = await send(900002, text=f"0007 {account_number}")
|
||||
assert "уже участвует" in output(calls)
|
||||
notifications = await send(ADMIN, callback=f"admin_conduct_confirmed_{lottery_id}")
|
||||
assert any(call.__api_method__ == "sendMessage" and getattr(call, "chat_id", None) == CLIENT
|
||||
and "Вы выиграли" in (getattr(call, "text", "") or "") for call in notifications)
|
||||
await send(900002, text=f"/verify_winner TEST1234 {lottery_id}")
|
||||
async with async_session_maker() as session:
|
||||
winner = await session.scalar(select(Winner))
|
||||
assert winner.is_claimed and winner.user_id == owner_id and winner.account_number == account_number
|
||||
assert_clean(caplog)
|
||||
|
||||
|
||||
async def test_card_cannot_replace_existing_participant_owner(caplog):
|
||||
lottery_id, _ = await seed(participants=False)
|
||||
async with async_session_maker() as session:
|
||||
other = User(telegram_id=960005, club_card_number="0008")
|
||||
session.add(other)
|
||||
await session.flush()
|
||||
other_id = other.id
|
||||
session.add(Participation(lottery_id=lottery_id, account_number=NEW_ACCOUNT, user_id=other_id))
|
||||
await session.commit()
|
||||
await send(900002, callback=f"cash_add:{lottery_id}")
|
||||
calls = await send(900002, text=f"0007 {NEW_ACCOUNT}")
|
||||
assert "другим владельцем" in output(calls)
|
||||
async with async_session_maker() as session:
|
||||
assert await session.scalar(select(Participation.user_id)) == other_id
|
||||
assert not await session.scalar(select(Account.id).where(Account.account_number == NEW_ACCOUNT))
|
||||
assert_clean(caplog)
|
||||
|
||||
|
||||
async def test_two_cashiers_repair_once_and_closed_draw_cannot_be_relinked():
|
||||
lottery_id, owner_id = await seed(participants=False)
|
||||
async with async_session_maker() as session:
|
||||
session.add(Participation(lottery_id=lottery_id, account_number=NEW_ACCOUNT))
|
||||
await session.commit()
|
||||
async def link():
|
||||
async with async_session_maker() as session:
|
||||
return await AccountParticipationService.add_account_to_lottery(
|
||||
session, lottery_id, f"0007 {NEW_ACCOUNT}", require_card=True
|
||||
)
|
||||
results = await asyncio.gather(link(), link())
|
||||
assert sum(bool(item.get("linked")) for item in results) == 1
|
||||
async with async_session_maker() as session:
|
||||
assert await session.scalar(select(func.count(Participation.id))) == 1
|
||||
assert await session.scalar(select(Participation.user_id)) == owner_id
|
||||
await LotteryService.conduct_draw(session, lottery_id)
|
||||
assert not (await link())["success"]
|
||||
|
||||
|
||||
async def test_shared_add_account_callback_cannot_drop_card_from_state(caplog):
|
||||
lottery_id, _ = await seed(participants=False)
|
||||
await context(900002).update_data(accounts=[{"account_number": ACCOUNT}])
|
||||
calls = await send(900002, callback=f"add_to_lottery_{lottery_id}")
|
||||
assert "обязательна клубная карта" in output(calls)
|
||||
async with async_session_maker() as session:
|
||||
assert await session.scalar(select(func.count(Participation.id))) == 0
|
||||
await context(900002).update_data(accounts=[{"club_card": "0007", "account_number": ACCOUNT}])
|
||||
await send(900002, callback=f"add_to_lottery_{lottery_id}")
|
||||
async with async_session_maker() as session:
|
||||
ticket = await session.scalar(select(Participation))
|
||||
assert ticket.user_id and ticket.account_id
|
||||
assert_clean(caplog)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("card", ["0007", "01234567890123", "0" * 49 + "1"])
|
||||
async def test_cashier_preserves_card_lengths_and_leading_zeroes(card, caplog):
|
||||
lottery_id, owner_id = await seed(participants=False)
|
||||
async with async_session_maker() as session:
|
||||
user = await session.get(User, owner_id)
|
||||
user.club_card_number = card
|
||||
await session.commit()
|
||||
await send(900002, callback=f"cash_add:{lottery_id}")
|
||||
await send(900002, text=f"{card}\t{NEW_ACCOUNT}")
|
||||
async with async_session_maker() as session:
|
||||
assert await session.scalar(select(Participation.user_id)) == owner_id
|
||||
assert_clean(caplog)
|
||||
|
||||
|
||||
def test_cashier_parser_keeps_table_and_viposnova_card_pairs():
|
||||
for text in (f"0007\t{ACCOUNT}", f"{ACCOUNT}\n0007", f"Viposnova\n{ACCOUNT}\n0007"):
|
||||
assert parse_cashier_records(text) == ([f"0007 {ACCOUNT}"], [])
|
||||
@@ -101,12 +101,12 @@ async def test_duplicate_account_reports_and_finishes_dialog(route, caplog):
|
||||
await send(callback="account_action:add_to_lottery")
|
||||
calls = await send(callback=f"add_accounts_to:{lottery_id}")
|
||||
elif route == "command":
|
||||
await context(actor).update_data(accounts=[{"account_number": ACCOUNT}])
|
||||
await context(actor).update_data(accounts=[{"club_card": "0007", "account_number": ACCOUNT}])
|
||||
calls = await send(callback=f"add_to_lottery_{lottery_id}")
|
||||
elif route == "cashier":
|
||||
await send(actor, text="/cashier")
|
||||
await send(actor, callback=f"cash_add:{lottery_id}")
|
||||
calls = await send(actor, text=ACCOUNT)
|
||||
calls = await send(actor, text=f"0007 {ACCOUNT}")
|
||||
else:
|
||||
await send(callback=f"admin_bulk_add_accounts_to_{lottery_id}")
|
||||
calls = await send(text=ACCOUNT)
|
||||
@@ -182,7 +182,7 @@ async def test_two_cashiers_share_draw_without_duplicate_tickets(caplog):
|
||||
await session.commit()
|
||||
for actor in (900002, 960002):
|
||||
await send(actor, callback=f"cash_add:{lottery_id}")
|
||||
calls = await asyncio.gather(send(900002, text=ACCOUNT), send(960002, text=ACCOUNT), send(CLIENT, text="/start"))
|
||||
calls = await asyncio.gather(send(900002, text=f"0007 {ACCOUNT}"), send(960002, text=f"0007 {ACCOUNT}"), send(CLIENT, text="/start"))
|
||||
assert_clean(caplog)
|
||||
assert sum("Добавлено: 1" in output(result) for result in calls) == 1
|
||||
async with async_session_maker() as session:
|
||||
|
||||
Reference in New Issue
Block a user