Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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())

Интеграция с Aiogram

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}")

Webhook (FastAPI)

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.

API Reference

SubscriptionClient

Метод Описание
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 кэша доступа (сек).

Utility functions

Функция Описание
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?

Data models

Класс Описание
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).

Exceptions

Исключение Описание
SubscriptionError Базовое.
TariffNotFoundError Тариф не найден.
SubscriptionNotFoundError Подписка не найдена.
PaymentError Ошибка платежа.
DatabaseError Ошибка БД.
WebhookValidationError Невалидный webhook.

Модель данных (Postgres)

Таблица Ключ Описание
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).

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages