subscription-lib v1.1 (YooKassa)
Библиотека для управления платными подписками в Telegram-ботах с интеграцией YooKassa.
Управление тарифами -- хранение тарифов в Postgres, авто-миграции при старте.
Подписки пользователей -- создание/продление, статус active / expired / canceled.
Административные действия -- ручная выдача, отмена подписки, просмотр подписчиков.
Интеграция с YooKassa :
создание платежей по тарифу;
обработка вебхуков payment.* и refund.* с идемпотентностью;
автоматическая отмена подписки при возврате средств.
Интеграция с Telegram :
генерация inline-клавиатур с тарифами;
конвертация в Aiogram / Pyrogram / raw;
parse_callback_data и validate_callback_sender для безопасного разбора.
Free trial -- grant_subscription(user_id, tariff_id, duration_days=7).
Аналитика -- get_revenue_stats(), get_user_payments().
Автоматизация -- get_expiring_subscriptions() для напоминаний, cleanup_expired() для cron.
Безопасность -- is_webhook_ip_trusted(), validate_callback_sender().
Async context manager -- async with SubscriptionClient(...) as sub: ...
Event callbacks -- @on_payment_success, @on_subscription_expired, @on_refund.
pip install . # обычная установка
pip install -e " .[dev]" # editable + dev-зависимости
pip install " .[telegram]" # + aiogram / pyrogram
python -m pip install --upgrade build && python -m build # wheel + sdist
Переменная
Описание
Пример
YOOKASSA_SHOP_ID
Shop ID из YooKassa
123456
YOOKASSA_SECRET_KEY
Секретный ключ
test_...
SUBSCRIPTIONS_DB_DSN
PostgreSQL DSN
postgresql://user:pass@host/db
import asyncio , os
from subscription_lib import SubscriptionClient , Tariff
async def main () -> None :
async with SubscriptionClient (
bot_id = "my_awesome_bot" ,
tariffs = [
Tariff (id = "monthly" , name = "Месячная" , price = 299 , duration_days = 30 ),
Tariff (id = "yearly" , name = "Годовая" , price = 2990 , duration_days = 365 ),
],
yookassa_shop_id = os .getenv ("YOOKASSA_SHOP_ID" , "" ),
yookassa_secret_key = os .getenv ("YOOKASSA_SECRET_KEY" , "" ),
db_config = {"dsn" : os .getenv ("SUBSCRIPTIONS_DB_DSN" )},
cache_ttl = 60 , # кэш доступа 60 сек (по умолчанию 300)
) as sub :
# Проверка доступа
print (await sub .has_access (123456 ))
# Создание платежа
payment = await sub .create_payment (123456 , "monthly" )
print (payment .url )
# Админ: выдать бесплатный триал на 7 дней
await sub .grant_subscription (123456 , "monthly" , duration_days = 7 )
# Админ: отменить подписку
await sub .cancel_subscription (123456 )
# Все активные подписчики
subs = await sub .get_active_subscribers ()
# Кто истекает через 3 дня?
expiring = await sub .get_expiring_subscriptions (within_days = 3 )
# История платежей
payments = await sub .get_user_payments (123456 )
# Выручка
stats = await sub .get_revenue_stats ()
print (f"Total: { stats .total_revenue } RUB, { stats .successful_payments } payments" )
asyncio .run (main ())
from aiogram import Router
from aiogram .types import Message , CallbackQuery
from subscription_lib import SubscriptionClient , parse_callback_data , validate_callback_sender
router = Router ()
subscription : SubscriptionClient = ... # создаётся при старте
@router .message (commands = {"buy" })
async def buy (message : Message ):
kb = subscription .create_payment_keyboard (message .from_user .id )
await message .answer ("Выберите тариф:" , reply_markup = kb .to_aiogram ())
@router .callback_query ()
async def pay (cq : CallbackQuery ):
parsed = parse_callback_data (cq .data or "" )
if not parsed :
return
bot_id , user_id , tariff_id = parsed
# Проверка от подмены callback_data (scenario 7.2)
if not validate_callback_sender (cq .data , cq .from_user .id ):
await cq .answer ("Access denied" , show_alert = True )
return
payment = await subscription .create_payment (user_id , tariff_id )
await cq .message .answer (f"Оплатите: { payment .url } " )
from fastapi import FastAPI , Request
from subscription_lib import SubscriptionClient , is_webhook_ip_trusted
app = FastAPI ()
subscription : SubscriptionClient = ...
@app .post ("/webhook/yookassa" )
async def webhook (request : Request ):
# Опционально: проверка IP источника
client_ip = request .client .host
if not is_webhook_ip_trusted (client_ip ):
return {"error" : "untrusted IP" }, 403
return await subscription .handle_yookassa_webhook (await request .json ())
Безопасность вебхуков:
HTTPS обязателен;
is_webhook_ip_trusted(ip) -- проверка CIDR-диапазонов YooKassa;
библиотека автоматически обрабатывает refund.succeeded -- отменяет подписку и вызывает @on_refund.
Метод
Описание
async initialize()
Создаёт пул, таблицы, фоновую задачу.
async close()
Закрывает пул, останавливает фон.
async has_access(user_id) -> bool
Есть ли активная подписка.
async get_subscription_info(user_id) -> UserSubscription | None
Детали подписки.
async create_payment(user_id, tariff_id) -> PaymentLink
Создать платёж.
async handle_yookassa_webhook(payload) -> dict
Обработать webhook.
async grant_subscription(user_id, tariff_id, duration_days=None) -> bool
Выдать подписку без оплаты.
async cancel_subscription(user_id) -> bool
Отменить подписку.
async get_active_subscribers() -> List[UserSubscription]
Все активные подписчики.
async get_expiring_subscriptions(within_days=3) -> List[UserSubscription]
Подписки, истекающие скоро.
async get_user_payments(user_id) -> List[PaymentRecord]
История платежей.
async get_revenue_stats(since=None) -> RevenueStats
Статистика выручки.
async cleanup_expired() -> List[int]
Ручная очистка истекших.
create_payment_keyboard(user_id, tariff_ids=None) -> PaymentKeyboard
Клавиатура.
@on_payment_success
Callback: (user_id, tariff_id).
@on_subscription_expired
Callback: (user_id).
@on_refund
Callback: (user_id, tariff_id).
Конструктор:
Параметр
Тип
По умолчанию
Описание
bot_id
str
--
ID бота.
tariffs
List[Tariff]
--
Тарифы.
yookassa_shop_id
str
--
Shop ID.
yookassa_secret_key
str
--
Ключ.
db_config
dict
None
{"dsn": "..."}
return_url
str
https://t.me/{bot_username}
URL возврата.
auto_create_tables
bool
True
Авто-таблицы.
cleanup_interval_seconds
int | None
3600
Интервал очистки.
cache_ttl
int
300
TTL кэша доступа (сек).
Функция
Описание
parse_callback_data(data) -> (bot_id, user_id, tariff_id) | None
Разбор yp:... строки.
validate_callback_sender(data, actual_user_id) -> bool
Проверка user_id vs отправитель.
is_webhook_ip_trusted(ip) -> bool
IP в диапазонах YooKassa?
Класс
Описание
Tariff
Тариф (id, name, price, duration_days, description).
UserSubscription
Подписка (user_id, tariff, status, start/end, is_active, days_remaining).
PaymentLink
Ссылка на оплату (url, payment_id, amount).
PaymentRecord
Запись о платеже (payment_id, user_id, tariff_id, amount, status, dates).
RevenueStats
Статистика (total_payments, successful_payments, total_revenue).
PaymentKeyboard
Клавиатура (to_aiogram, to_pyrogram, get_raw_buttons).
Исключение
Описание
SubscriptionError
Базовое.
TariffNotFoundError
Тариф не найден.
SubscriptionNotFoundError
Подписка не найдена.
PaymentError
Ошибка платежа.
DatabaseError
Ошибка БД.
WebhookValidationError
Невалидный webhook.
Таблица
Ключ
Описание
tariffs
(bot_id, tariff_id)
Тарифы бота.
subscriptions
UNIQUE(bot_id, user_id)
Один тариф на пользователя.
payments
payment_id UNIQUE
Платежи.
Все даты в UTC.
pip install -e " .[dev]"
pytest -v
Не требуют Postgres / YooKassa. Покрывают модели, клавиатуры, вебхуки,
callback_data, исключения, IP-проверку.
PostgreSQL only (asyncpg).
Один активный тариф на пользователя per bot_id.
Рекуррентные платежи не встроены (реализуются поверх API с YooKassa saved methods).