Урок 6.1 — Invoice Polling
Изучим механизм polling для отслеживания оплаты инвойсов: декораторы, ручной poll(), автоматический start_polling() и полный жизненный цикл инвойса.
Polling (опрос) — это механизм, при котором ваш клиент периодически отправляет запросы к Crypto Pay API, чтобы узнать, изменился ли статус инвойса. В aiosend polling реализован через декораторы @cp.invoice_paid() и @cp.invoice_expired(), а также методы invoice.poll() и cp.start_polling(). В этом уроке мы разберём все способы отслеживать оплату счетов.
Что такое Polling в aiosend
Polling — это активный опрос сервера на предмет обновлений. В контексте aiosend polling означает, что ваш код периодически проверяет статус инвойса (или чека) через Crypto Pay API, пока он не будет оплачен или не истечёт.
aiosend предоставляет два подхода к polling:
Автоматический (декораторы)
@cp.invoice_paid()@cp.invoice_expired()- Регистрируются на старте
- Запуск через
cp.start_polling() - Обработка всех инвойсов сразу
Ручной (метод poll)
invoice.poll()- Ждёт оплаты конкретного инвойса
- Возвращает обновлённый
Invoice - Таймаут и задержка из
PollingConfig - Подходит для простых сценариев
В основе обоих подходов лежит один и тот же внутренний механизм: периодические запросы к getInvoices с фильтрацией по статусу. Разница лишь в уровне абстракции.
Декоратор @cp.invoice_paid()
Декоратор @cp.invoice_paid() регистрирует функцию-обработчик, которая будет вызвана, когда инвойс будет оплачен. Это основной способ реакции на оплату в aiosend.
Сигнатура обработчика:
@cp.invoice_paid()
async def on_invoice_paid(invoice: Invoice, **kwargs) -> None:
"""Обработчик оплаченного инвойса.
Аргументы:
invoice: объект Invoice с обновлённым статусом PAID
**kwargs: дополнительные аргументы от фильтров
"""
pass
Параметр invoice содержит полную информацию о счёте: сумму, валюту, кто оплатил, когда, скрытое сообщение и т.д. **kwargs используется для передачи данных из фильтров (мы рассмотрим их в уроке 6.4).
import asyncio
from aiosend import CryptoPay
cp = CryptoPay(token="YOUR_TOKEN")
@cp.invoice_paid()
async def on_paid(invoice, **kwargs):
print(f"✅ Инвойс #{invoice.invoice_id} оплачен!")
print(f" Сумма: {invoice.amount} {invoice.asset}")
print(f" Покупатель: {invoice.paid_by_user_id}")
print(f" Время оплаты: {invoice.paid_at}")
if invoice.hidden_message:
print(f" Секретное сообщение: {invoice.hidden_message}")
@cp.invoice_expired()
async def on_expired(invoice, **kwargs):
print(f"❌ Инвойс #{invoice.invoice_id} истёк")
async def main():
# Создаём инвойс
invoice = await cp.create_invoice(
amount=10,
asset="USDT",
description="Тестовый платёж",
hidden_message="Спасибо за оплату!",
)
print(f"Счёт создан: {invoice.bot_invoice_url}")
# Запускаем polling
await cp.start_polling()
asyncio.run(main())
Когда инвойс будет оплачен покупателем, aiosend автоматически обнаружит это через polling и вызовет on_paid. Если срок инвойса истечёт — будет вызван on_expired.
🔍 Как работает invoice_paid под капотом?
Декоратор добавляет функцию-обработчик в PollingManager. Когда запускается start_polling(), менеджер в цикле вызывает getInvoices(status="paid") и для каждого нового оплаченного инвойса вызывает зарегистрированные обработчики. Аналогично для expired — через getInvoices(status="expired").
Декоратор @cp.invoice_expired()
Декоратор @cp.invoice_expired() работает аналогично invoice_paid, но реагирует на инвойсы, у которых истёк срок оплаты. Это важно для очистки и обработки просроченных счетов.
@cp.invoice_expired()
async def on_invoice_expired(invoice: Invoice, **kwargs) -> None:
"""Обработчик истёкшего инвойса.
Аргументы:
invoice: объект Invoice со статусом EXPIRED
**kwargs: дополнительные аргументы от фильтров
"""
# Логика обработки просрочки:
# - отмена заказа в БД
# - возврат средств (если была предоплата)
# - уведомление пользователя
pass
⚠️ Важно: порядок обработки
aiosend обрабатывает инвойсы в порядке их создания (по created_at). Если вы создали 10 инвойсов, и 5 из них оплачены, а 5 просрочены — polling сначала найдёт оплаченные, потом просроченные по мере поступления. Обработчики вызываются последовательно, но вы можете сделать их асинхронными для параллельной обработки.
На практике invoice_expired используется для:
- Помечения заказа как «не оплачен» в вашей БД
- Отправки уведомления покупателю о просрочке
- Освобождения зарезервированных товаров/услуг
- Логирования статистики просроченных платежей
import asyncio
import logging
from datetime import datetime
from aiosend import CryptoPay
from aiosend.polling import PollingConfig
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
cp = CryptoPay(
token="YOUR_TOKEN",
polling_config=PollingConfig(timeout=120, delay=2),
)
paid_orders = set()
@cp.invoice_paid()
async def handle_paid(invoice, **kwargs):
logger.info(f"💵 Инвойс {invoice.invoice_id} оплачен!")
order_id = invoice.payload or "unknown"
paid_orders.add(order_id)
# Здесь: выдача товара, активация подписки и т.д.
await process_order(order_id, invoice)
@cp.invoice_expired()
async def handle_expired(invoice, **kwargs):
logger.warning(f"⏰ Инвойс {invoice.invoice_id} просрочен!")
order_id = invoice.payload or "unknown"
# Здесь: отмена заказа, уведомление пользователя
await cancel_order(order_id)
async def process_order(order_id, invoice):
# Имитация обработки заказа
await asyncio.sleep(0.5)
logger.info(f"✅ Заказ {order_id} обработан!")
async def cancel_order(order_id):
# Имитация отмены
logger.info(f"❌ Заказ {order_id} отменён из-за просрочки")
async def main():
# Создаём тестовый инвойс
invoice = await cp.create_invoice(
amount=25,
asset="USDT",
description="Премиум-доступ",
payload=f"order_{datetime.now().timestamp()}",
expires_in=120, # 2 минуты на оплату
)
logger.info(f"Счёт: {invoice.bot_invoice_url}")
logger.info(f"Ожидание оплаты... (даётся 2 минуты)")
await cp.start_polling()
if __name__ == "__main__":
asyncio.run(main())
Метод invoice.poll() — ручное ожидание оплаты
Метод invoice.poll() позволяет дождаться оплаты конкретного инвойса. Это простой способ, когда вам не нужны декораторы и вы хотите просто «заморозить» выполнение до оплаты.
async def poll(
self,
timeout: int | None = None,
delay: float | None = None,
) -> Invoice:
"""Ожидает оплаты инвойса.
Аргументы:
timeout: максимальное время ожидания (переопределяет PollingConfig)
delay: задержка между проверками (переопределяет PollingConfig)
Возвращает:
Invoice с обновлённым статусом (PAID или EXPIRED)
"""
...
Метод poll() внутри циклически вызывает get_invoices(invoice_ids=[self.invoice_id]) и проверяет статус. Как только статус меняется на PAID или EXPIRED, метод возвращает обновлённый объект Invoice.
import asyncio
from aiosend import CryptoPay
async def main():
cp = CryptoPay(token="YOUR_TOKEN")
# Создаём инвойс
invoice = await cp.create_invoice(
amount=5,
asset="USDT",
description="Оплата за кофе ☕",
)
print(f"Счёт: {invoice.bot_invoice_url}")
print("Ожидаем оплату...")
# Ждём оплаты (блокирует выполнение до оплаты)
result = await invoice.poll(timeout=300, delay=2)
if result.status == "paid":
print(f"✅ Оплачено! Сумма: {result.paid_amount} {result.paid_asset}")
else:
print(f"❌ Счёт истёк: {result.status}")
asyncio.run(main())
💡 Когда использовать invoice.poll()?
Метод poll() удобен для простых скриптов и консольных утилит. Например, в Telegram-боте вы можете создать инвойс и сразу запустить poll() в отдельной таске для каждого пользователя. Однако для высоконагруженных систем предпочтительнее использовать декораторы, так как они масштабируются лучше.
Параметры timeout и delay переопределяют соответствующие значения из PollingConfig клиента. Если не переданы — используются значения по умолчанию (300 секунд timeout, 2 секунды delay).
Метод cp.start_polling() — запуск поллинга
Метод cp.start_polling() запускает бесконечный цикл опроса API. Он обрабатывает все зарегистрированные через декораторы обработчики (@cp.invoice_paid(), @cp.invoice_expired(), @cp.check_activated() и т.д.).
async def start_polling(
self,
polling_config: PollingConfig | None = None,
reset_webhook: bool = True,
) -> None:
"""Запускает polling-цикл.
Аргументы:
polling_config: конфигурация (переопределяет переданную в конструктор)
reset_webhook: если True, сначала сбрасывает вебхуки
"""
...
Как только start_polling() запущен, он работает до тех пор, пока не будет прерван (например, KeyboardInterrupt). Внутри он использует PollingManager, который управляет циклом:
async def start_polling(self, polling_config=None, reset_webhook=True):
config = polling_config or self._polling_config
if reset_webhook:
await self._delete_webhook()
manager = PollingManager(self, config)
await manager.run()
# PollingManager.run() внутри:
async def run(self):
while True:
try:
# Проверяем новые оплаченные инвойсы
paid = await self.client.get_invoices(status="paid")
for invoice in paid:
await self._process_paid(invoice)
# Проверяем новые просроченные инвойсы
expired = await self.client.get_invoices(status="expired")
for invoice in expired:
await self._process_expired(invoice)
# Проверяем новые чеки (см. урок 6.2)
activated = await self.client.get_checks(status="activated")
for check in activated:
await self._process_activated(check)
# Ждём перед следующим запросом
await asyncio.sleep(self.config.delay)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Polling error: {e}")
await asyncio.sleep(self.config.delay)
⚠️ start_polling() и reset_webhook=True
По умолчанию reset_webhook=True. Это значит, что перед запуском polling aiosend отправит запрос deleteWebhook в Crypto Pay API, чтобы отключить вебхуки. Если вы используете вебхуки, передайте reset_webhook=False.
import asyncio
from aiosend import CryptoPay
from aiosend.polling import PollingConfig
cp = CryptoPay(
token="YOUR_TOKEN",
polling_config=PollingConfig(timeout=600, delay=3),
)
@cp.invoice_paid()
async def on_paid(invoice, **kwargs):
print(f"Инвойс {invoice.invoice_id} оплачен!")
async def main():
# Переопределяем конфиг при запуске
await cp.start_polling(
polling_config=PollingConfig(timeout=120, delay=1),
reset_webhook=False,
)
# Этот код будет работать вечно, пока не прервать его
asyncio.run(main())
Жизненный цикл инвойса с polling
Давайте проследим полный путь инвойса от создания до финального статуса с использованием polling:
Диаграмма жизненного цикла инвойса
┌─────────────────────────────────────────────┐
│ 1. Создание инвойса │
│ cp.create_invoice(amount, asset, ...) │
│ → status = ACTIVE │
└────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 2. Запуск Polling │
│ cp.start_polling() или invoice.poll() │
│ → периодический опрос getInvoices() │
│ → задержка между запросами (delay=2s) │
└────────────────┬────────────────────────────┘
│
┌─────────────┴─────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ 3a. Оплачен │ │ 3b. Истёк срок │
│ Покупатель платит │ │ expires_in истекло │
│ → status = PAID │ │ → status = EXPIRED │
│ Вызов on_paid() │ │ Вызов on_expired() │
│ invoice_paid декоратор│ │ invoice_expired │
└─────────────────────┘ └─────────────────────┘
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ 4a. Обработка │ │ 4b. Обработка │
│ Выдача товара/услуги │ │ Отмена заказа │
│ Активация подписки │ │ Уведомление │
│ Логирование │ │ Освобождение товара │
└─────────────────────┘ └─────────────────────┘
Разберём каждый шаг подробно:
| Шаг | Что происходит | Статус инвойса | Код |
|---|---|---|---|
| 1 | Создание счёта | ACTIVE |
create_invoice() |
| 2 | Запуск polling, ожидание | ACTIVE |
start_polling() |
| 3a | Покупатель оплатил | PAID |
Декоратор @cp.invoice_paid() |
| 3b | Срок истёк | EXPIRED |
Декоратор @cp.invoice_expired() |
| 4a | Бизнес-логика: выдача товара | PAID |
Ваша функция-обработчик |
| 4b | Бизнес-логика: отмена | EXPIRED |
Ваша функция-обработчик |
Важно понимать: poll() и start_polling() используют один и тот же внутренний механизм — PollingManager. Разница в том, что poll() ждёт конкретный инвойс, а start_polling() обрабатывает все инвойсы через декораторы.
💡 Polling in background
Вы можете запустить start_polling() как фоновую задачу с помощью asyncio.create_task(). Это позволяет вашему приложению продолжать работу (например, отвечать на команды бота) параллельно с polling.
Полный пример: бот с обработкой платежей
Давайте соберём всё вместе и напишем полноценный скрипт, который создаёт инвойс, запускает polling в фоне и обрабатывает оплату/просрочку.
import asyncio
import logging
from aiosend import CryptoPay
from aiosend.polling import PollingConfig
from aiosend.exceptions import APIError
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger(__name__)
class PaymentBot:
"""Простой бот для приёма платежей через Crypto Pay."""
def __init__(self, token: str):
self.cp = CryptoPay(
token=token,
polling_config=PollingConfig(timeout=300, delay=2),
)
self._register_handlers()
def _register_handlers(self):
@self.cp.invoice_paid()
async def on_paid(invoice, **kwargs):
logger.info(f"💰 Получен платёж #{invoice.invoice_id}")
logger.info(f" Сумма: {invoice.amount} {invoice.asset}")
logger.info(f" Payload: {invoice.payload}")
# Извлекаем user_id из payload
if invoice.payload and invoice.payload.startswith("user_"):
user_id = invoice.payload.split("_")[1]
logger.info(f" Пользователь #{user_id} оплатил!")
# Здесь: активация подписки, отправка уведомления и т.д.
await self.activate_subscription(int(user_id), invoice)
@self.cp.invoice_expired()
async def on_expired(invoice, **kwargs):
logger.warning(f"⏰ Платёж #{invoice.invoice_id} просрочен")
if invoice.payload and invoice.payload.startswith("user_"):
user_id = invoice.payload.split("_")[1]
await self.notify_expired(int(user_id), invoice)
async def activate_subscription(self, user_id: int, invoice):
"""Активирует подписку для пользователя."""
# Имитация бизнес-логики
await asyncio.sleep(0.3)
logger.info(f" ✅ Подписка активирована для user #{user_id}")
async def notify_expired(self, user_id: int, invoice):
"""Уведомляет об истечении срока."""
logger.info(f" 📧 Уведомление отправлено user #{user_id}")
async def create_payment(self, user_id: int, amount: float,
description: str = "") -> str:
"""Создаёт инвойс для пользователя."""
invoice = await self.cp.create_invoice(
amount=amount,
asset="USDT",
description=description or f"Оплата для user #{user_id}",
payload=f"user_{user_id}",
expires_in=600, # 10 минут
)
logger.info(f"📄 Счёт создан: #{invoice.invoice_id}")
return invoice.bot_invoice_url
async def run(self):
"""Запускает polling в фоне."""
logger.info("🚀 Запуск polling...")
try:
await self.cp.start_polling()
except asyncio.CancelledError:
logger.info("Polling остановлен")
async def main():
bot = PaymentBot(token="YOUR_TOKEN")
# Создаём тестовый платёж
url = await bot.create_payment(
user_id=12345,
amount=15,
description="Премиум-доступ на 1 месяц",
)
print(f"Ссылка на оплату: {url}")
# Запускаем обработку платежей
polling_task = asyncio.create_task(bot.run())
# Ждём завершения (по Ctrl+C)
try:
await polling_task
except KeyboardInterrupt:
polling_task.cancel()
await polling_task
if __name__ == "__main__":
asyncio.run(main())
✅ Что мы получили?
Класс PaymentBot инкапсулирует всю логику работы с платежами: создание инвойсов, обработку оплат и просрочек. Мы можем создать несколько экземпляров для разных токенов или добавить методы для разных типов платежей. Polling работает в фоновой задаче, не блокируя основной поток.
Дополнительные примеры использования
Рассмотрим ещё несколько практических сценариев, демонстрирующих гибкость polling в aiosend.
Пример: Множественная обработка с фильтрацией
Вы можете зарегистрировать несколько обработчиков на одно и то же событие. Все они будут вызваны последовательно:
import asyncio
from aiosend import CryptoPay
from aiosend.polling import PollingConfig
cp = CryptoPay(
token="YOUR_TOKEN",
polling_config=PollingConfig(timeout=300, delay=2),
)
# Первый обработчик — логирование
@cp.invoice_paid()
async def log_payment(invoice, **kwargs):
print(f"[LOG] Инвойс #{invoice.invoice_id} оплачен")
print(f"[LOG] Сумма: {invoice.amount} {invoice.asset}")
# Второй обработчик — бизнес-логика
@cp.invoice_paid()
async def process_payment(invoice, **kwargs):
print(f"[PROCESS] Обработка заказа для инвойса #{invoice.invoice_id}")
await asyncio.sleep(0.5) # имитация работы
print(f"[PROCESS] Заказ обработан!")
# Третий обработчик — уведомление
@cp.invoice_paid()
async def notify_user(invoice, **kwargs):
print(f"[NOTIFY] Отправка уведомления о платеже #{invoice.invoice_id}")
async def main():
invoice = await cp.create_invoice(amount=10, asset="USDT")
print(f"Счёт: {invoice.bot_invoice_url}")
await cp.start_polling()
asyncio.run(main())
Пример: Polling с таймаутом и retry
Иногда нужно перезапустить polling при ошибках. Вот шаблон с обработкой исключений:
import asyncio
import logging
from aiosend import CryptoPay
from aiosend.polling import PollingConfig
from aiosend.exceptions import APIError
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def run_polling_with_retry(cp: CryptoPay, max_retries: int = 3):
"""Запускает polling с автоматическим перезапуском при ошибках."""
retries = 0
while retries < max_retries:
try:
logger.info(f"Запуск polling (попытка {retries + 1})")
await cp.start_polling()
except APIError as e:
retries += 1
logger.error(f"Ошибка API: {e}")
if retries >= max_retries:
logger.critical("Превышено количество попыток")
raise
wait = 2 ** retries # exponential backoff
logger.info(f"Повтор через {wait} секунд...")
await asyncio.sleep(wait)
except asyncio.CancelledError:
logger.info("Polling остановлен")
break
@cp.invoice_paid()
async def on_paid(invoice, **kwargs):
logger.info(f"Инвойс #{invoice.invoice_id} оплачен!")
async def main():
cp = CryptoPay(token="YOUR_TOKEN")
try:
await run_polling_with_retry(cp)
except KeyboardInterrupt:
logger.info("Завершение...")
asyncio.run(main())
Ошибки и граничные случаи
Рассмотрим типичные проблемы при работе с polling и способы их решения.
Ошибка 1: PollingTimeoutError
Инвойс не был оплачен в течение polling_config.timeout.
# invoice.poll() с маленьким таймаутом
invoice = await cp.create_invoice(amount=10, asset="USDT")
try:
result = await invoice.poll(timeout=10) # всего 10 секунд
except PollingTimeoutError:
print("Не дождались оплаты за 10 секунд")
# Решение: увеличьте timeout или используйте декораторы
Ошибка 2: Двойная обработка одного инвойса
Если вы зарегистрируете несколько @cp.invoice_paid(), все они будут вызваны для одного инвойса.
@cp.invoice_paid()
async def handler1(invoice, **kwargs):
print("Handler 1") # будет вызван
@cp.invoice_paid()
async def handler2(invoice, **kwargs):
print("Handler 2") # тоже будет вызван!
# Оба обработчика получат один и тот же инвойс.
# Если нужна единственная обработка — используйте
# флаги в БД или ID инвойса.
Ошибка 3: Забыли запустить start_polling()
Декораторы регистрируют обработчики, но они не будут вызваны, пока не запущен start_polling().
cp = CryptoPay("TOKEN")
@cp.invoice_paid()
async def on_paid(invoice, **kwargs):
print("Никогда не будет вызвано :(")
# Забыли:
# await cp.start_polling()
# Решение: всегда вызывайте start_polling() после регистрации
# всех обработчиков
Граничный случай: Инвойс оплачен до запуска polling
Если покупатель оплатил инвойс до того, как вы запустили start_polling(), обработчик invoice_paid всё равно будет вызван, потому что при запуске polling проверяет все оплаченные инвойсы. Это гарантирует, что ни один платёж не будет пропущен.
Что важно запомнить
@cp.invoice_paid(), @cp.invoice_expired()) для автоматической обработки и invoice.poll() для ручного ожидания конкретного инвойса.async def handler(invoice: Invoice, **kwargs). invoice содержит все данные о счёте, **kwargs — для фильтров.timeout (макс. время ожидания) и delay (задержка между запросами). По умолчанию 300s / 2s.reset_webhook=False.Практическая задача
Задача: Система обработки платежей с polling
Напишите скрипт payment_system.py, который:
- Создаёт клиента CryptoPay с кастомным PollingConfig (timeout=600, delay=3).
- Регистрирует два обработчика через декораторы:
invoice_paidиinvoice_expired. - В обработчике
paidвыводит: ID инвойса, сумму, валюту, payload, время оплаты. - В обработчике
expiredвыводит: ID инвойса, причину просрочки, предлагает создать новый счёт. - Создаёт три инвойса с разными суммами и payload (для разных товаров).
- Запускает polling и обрабатывает все платежи.
- Реализует корректную обработку
KeyboardInterruptдля graceful shutdown.
Ожидаемый вывод при оплате инвойса:
✅ Инвойс #12345 оплачен!
Сумма: 25.00 USDT
Payload: subscription_premium
Оплачено в: 2024-12-01T12:34:56+00:00
❌ Инвойс #12346 истёк
Payload: subscription_basic
Создайте новый счёт для этого пользователя
Урок 6.1: Invoice Polling
8 вопросов