Skip to content

Управление подписками

Подписки (subscriptions) — это связи между абонентами и пакетами каналов, которые определяют, к каким каналам имеет доступ каждый абонент. Система подписок является ключевым механизмом монетизации IPTV-сервиса в Catena.

Что такое подписка

Подписка в Catena — это активная связь между абонентом и пакетом каналов. Когда у абонента есть подписка на пакет, он автоматически получает доступ ко всем каналам, входящим в этот пакет.

Ключевая концепция:

Абонент → Подписка → Пакет → Каналы → Просмотр
  1. Абонент подписывается на один или несколько пакетов
  2. Каждый пакет содержит набор каналов
  3. Абонент получает доступ ко всем каналам из всех своих пакетов
  4. При попытке просмотра система проверяет наличие подписки

Основные возможности:

  • Гибкое управление доступом — подключение и отключение пакетов в реальном времени
  • Множественные подписки — абонент может быть подписан на несколько пакетов одновременно
  • Бесплатные пакеты — автоматический доступ к базовому контенту для всех абонентов
  • Журналирование — полная история всех изменений подписок
  • API-first подход — простая интеграция с биллинговыми системами

Типичный workflow:

  1. Биллинговая система получает оплату от пользователя
  2. Биллинг вызывает API Catena для создания подписки
  3. Catena немедленно предоставляет доступ к каналам пакета
  4. Абонент начинает смотреть каналы
  5. По окончании периода биллинг отключает подписку
  6. Доступ к платным каналам автоматически блокируется

Жизненный цикл подписки

Создание подписки

Когда создаётся подписка:

  • При оплате пакета через биллинговую систему
  • При ручном подключении администратором
  • При активации промо-кода или бонуса
  • При предоставлении пробного периода

Что происходит при создании:

  1. Создаётся запись в базе данных о связи абонент-пакет
  2. Абонент немедленно получает доступ ко всем каналам пакета
  3. Запись добавляется в журнал операций (тип createPackageSubscriber)
  4. При следующем запросе плеера список доступных каналов обновляется

Активная подписка

Во время действия подписки:

  • Абонент может смотреть все каналы из пакета без ограничений
  • Система логирует все сеансы просмотра
  • Поле packages у абонента содержит ID активных пакетов
  • Streaming-сервер проверяет права доступа при каждом запросе потока

Отключение подписки

Когда отключается подписка:

  • По истечении оплаченного периода
  • При отмене подписки пользователем
  • При блокировке абонента администратором
  • При удалении пакета из системы

Что происходит при отключении:

  1. Удаляется запись о связи абонент-пакет
  2. Абонент немедленно теряет доступ к каналам этого пакета
  3. Запись добавляется в журнал операций (тип deletePackageSubscriber)
  4. Активные сеансы просмотра каналов пакета прерываются

Создание подписки

Через веб-интерфейс

  1. Откройте карточку абонента в разделе "Абоненты"
  2. Перейдите на вкладку "Подписки" или "Пакеты"
  3. Нажмите "Добавить подписку"
  4. Выберите пакет из выпадающего списка доступных пакетов
  5. Подтвердите добавление

Абонент немедленно получит доступ ко всем каналам выбранного пакета.

Через Management API

Создать подписку абонента на пакет:

curl -X POST https://your-catena-domain.com/tv-management/api/v1/packages-subscribers \
  -H "X-Auth-Token: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "subscriberId": "sKl9SW3AAAE.",
    "packageId": "pKl9SW3AAAE."
  }'

Параметры запроса:

  • subscriberId (обязательно) — ID абонента, которому подключается пакет
  • packageId (обязательно) — ID пакета для подключения

Ответ:

{
  "subscriberId": "sKl9SW3AAAE.",
  "packageId": "pKl9SW3AAAE.",
  "portalId": "pKl9SW3AAAE."
}

Важные моменты:

  • Абонент и пакет должны принадлежать одному порталу
  • Если подписка уже существует, API вернёт ошибку
  • Изменения вступают в силу немедленно
  • Операция записывается в журнал

Удаление подписки

Через веб-интерфейс

  1. Откройте карточку абонента
  2. Перейдите на вкладку "Подписки"
  3. Найдите пакет в списке активных подписок
  4. Нажмите "Удалить" или "Отключить"
  5. Подтвердите отключение

Абонент немедленно потеряет доступ к каналам этого пакета.

Через Management API

Удалить подписку абонента на пакет:

curl -X DELETE https://your-catena-domain.com/tv-management/api/v1/packages-subscribers \
  -H "X-Auth-Token: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "subscriberId": "sKl9SW3AAAE.",
    "packageId": "pKl9SW3AAAE."
  }'

Параметры запроса:

  • subscriberId (обязательно) — ID абонента
  • packageId (обязательно) — ID пакета для отключения

Ответ:

HTTP 201 - подписка удалена

Важные моменты:

  • Если подписки не существует, API вернёт ошибку
  • Активные сеансы просмотра будут прерваны
  • Изменения вступают в силу немедленно
  • Операция записывается в журнал

Просмотр подписок

Подписки конкретного абонента

Получить список пакетов абонента:

curl -X GET https://your-catena-domain.com/tv-management/api/v1/subscribers/sKl9SW3AAAE. \
  -H "X-Auth-Token: your-api-key"

Ответ:

{
  "subscriberId": "sKl9SW3AAAE.",
  "portalId": "pKl9SW3AAAE.",
  "name": "Иван Петров",
  "phoneCountryCode": "7",
  "phone": "9161234567",
  "playback_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "packages": ["pKl9SW3AAAE.", "sportKl9SW3AAAE."]
}

Поле packages содержит массив ID всех пакетов, на которые подписан абонент.

Подписчики конкретного пакета

К сожалению, прямого API для получения списка абонентов пакета нет. Используйте журнал операций или получите всех абонентов и отфильтруйте по packages:

# Получить всех абонентов
curl -X GET https://your-catena-domain.com/tv-management/api/v1/subscribers \
  -H "X-Auth-Token: your-api-key" \
  | jq '.subscribers[] | select(.packages[] | contains("pKl9SW3AAAE."))'

История подписок через журнал операций

Получить все операции с подписками конкретного абонента:

curl -X GET "https://your-catena-domain.com/tv-management/api/v1/operations?subscriberId=sKl9SW3AAAE.&type=createPackageSubscriber&type=deletePackageSubscriber" \
  -H "X-Auth-Token: your-api-key"

Получить операции с конкретным пакетом:

curl -X GET "https://your-catena-domain.com/tv-management/api/v1/operations?packageId=pKl9SW3AAAE." \
  -H "X-Auth-Token: your-api-key"

Ответ:

{
  "operations": [
    {
      "operationId": "opKl9SW3AAAE.",
      "type": "createPackageSubscriber",
      "subscriberId": "sKl9SW3AAAE.",
      "packageId": "pKl9SW3AAAE.",
      "portalId": "pKl9SW3AAAE.",
      "createdAt": "2024-10-16T10:05:00Z",
      "payload": {
        "packageId": "pKl9SW3AAAE.",
        "subscriberId": "sKl9SW3AAAE."
      }
    },
    {
      "operationId": "opKl9SW3AAAB.",
      "type": "deletePackageSubscriber",
      "subscriberId": "sKl9SW3AAAE.",
      "packageId": "pKl9SW3AAAE.",
      "portalId": "pKl9SW3AAAE.",
      "createdAt": "2024-10-20T15:30:00Z",
      "payload": {
        "packageId": "pKl9SW3AAAE.",
        "subscriberId": "sKl9SW3AAAE."
      }
    }
  ],
  "next": "cursor-for-next-page"
}

Типы операций:

  • createPackageSubscriber — создание подписки
  • deletePackageSubscriber — удаление подписки
  • autoCreateSubscriber — автоматическое создание абонента (может включать подписку на базовый пакет)

Бесплатные пакеты портала

Catena поддерживает концепцию "бесплатных пакетов" — пакетов, которые автоматически доступны всем абонентам портала без явного создания подписки.

Концепция бесплатных пакетов

Как это работает:

  • В настройках портала определяется список бесплатных пакетов
  • Все абоненты портала автоматически получают доступ к каналам из этих пакетов
  • Не требуется создавать индивидуальные подписки для каждого абонента
  • Идеально для базового контента, демо-каналов, рекламных каналов

Применение:

  • Базовый контент — федеральные каналы, доступные всем
  • Пробный период — демо-контент для новых пользователей
  • Промо-каналы — рекламные и информационные каналы
  • Социально значимые каналы — обязательные к распространению каналы

Управление бесплатными пакетами

Просмотр бесплатных пакетов портала:

curl -X GET https://your-catena-domain.com/tv-management/api/v1/portal \
  -H "X-Auth-Token: your-api-key"

Ответ:

{
  "portalId": "pKl9SW3AAAE.",
  "name": "my-iptv-portal",
  "domain": "iptv.example.com",
  "freePackages": ["basicKl9SW3AAAE.", "demoKl9SW3AAAE."],
  "branding": {
    "title": "My IPTV Service",
    "description": "Premium IPTV streaming"
  }
}

Добавить пакет в список бесплатных:

curl -X POST https://your-catena-domain.com/tv-management/api/v1/portal/free-packages/basicKl9SW3AAAE. \
  -H "X-Auth-Token: your-api-key"

Удалить пакет из списка бесплатных:

curl -X DELETE https://your-catena-domain.com/tv-management/api/v1/portal/free-packages/basicKl9SW3AAAE. \
  -H "X-Auth-Token: your-api-key"

Важно:

  • Изменения в бесплатных пакетах применяются ко всем абонентам мгновенно
  • При добавлении — все абоненты получают доступ к каналам пакета
  • При удалении — доступ теряют только те, у кого нет явной подписки

Интеграция с биллинговыми системами

Архитектура интеграции

Типичная схема:

[Биллинговая система] ←→ [API Catena] ←→ [Streaming сервер]
         ↓                      ↓                  ↓
    Платежи              Подписки           Доступ к каналам

Ответственность биллинга:

  • Приём платежей от пользователей
  • Управление тарифами и периодами подписки
  • Отслеживание окончания подписок
  • Вызов API Catena для подключения/отключения пакетов

Ответственность Catena:

  • Управление доступом к каналам
  • Проверка прав при просмотре
  • Логирование активности абонентов
  • Предоставление статистики просмотров

Примеры интеграции

Пример 1: Webhook при оплате

Биллинг отправляет webhook в ваш сервис при успешной оплате:

from flask import Flask, request
import requests

app = Flask(__name__)

CATENA_API_URL = "https://catena.example.com/tv-management/api/v1"
CATENA_API_KEY = "your-api-key"

@app.route('/billing-webhook', methods=['POST'])
def billing_webhook():
    data = request.json

    if data['event'] == 'payment.success':
        # Получили оплату - активируем подписку
        subscriber_id = get_subscriber_id(data['user_phone'])
        package_id = get_package_id(data['tariff_name'])

        # Создаём подписку в Catena
        response = requests.post(
            f"{CATENA_API_URL}/packages-subscribers",
            headers={"X-Auth-Token": CATENA_API_KEY},
            json={
                "subscriberId": subscriber_id,
                "packageId": package_id
            }
        )

        if response.status_code == 200:
            return {"status": "ok", "message": "Subscription activated"}
        else:
            return {"status": "error", "message": response.text}, 500

    elif data['event'] == 'subscription.expired':
        # Подписка истекла - деактивируем
        subscriber_id = get_subscriber_id(data['user_phone'])
        package_id = get_package_id(data['tariff_name'])

        # Удаляём подписку в Catena
        response = requests.delete(
            f"{CATENA_API_URL}/packages-subscribers",
            headers={"X-Auth-Token": CATENA_API_KEY},
            json={
                "subscriberId": subscriber_id,
                "packageId": package_id
            }
        )

        return {"status": "ok", "message": "Subscription deactivated"}

    return {"status": "ok"}

def get_subscriber_id(phone):
    """Получить ID абонента Catena по номеру телефона"""
    response = requests.get(
        f"{CATENA_API_URL}/subscribers",
        headers={"X-Auth-Token": CATENA_API_KEY}
    )
    subscribers = response.json()['subscribers']

    for sub in subscribers:
        full_phone = f"+{sub['phoneCountryCode']}{sub['phone']}"
        if full_phone == phone:
            return sub['subscriberId']

    # Если абонент не найден - создаём
    return create_subscriber(phone)

def get_package_id(tariff_name):
    """Сопоставить название тарифа с ID пакета"""
    tariff_mapping = {
        "basic": "basicKl9SW3AAAE.",
        "premium": "premiumKl9SW3AAAE.",
        "sport": "sportKl9SW3AAAE."
    }
    return tariff_mapping.get(tariff_name)

if __name__ == '__main__':
    app.run(port=5000)

Пример 2: Периодическая синхронизация

Регулярная проверка и синхронизация подписок:

import requests
from datetime import datetime, timedelta

CATENA_API_URL = "https://catena.example.com/tv-management/api/v1"
CATENA_API_KEY = "your-api-key"
BILLING_DB = "postgresql://billing_db"

def sync_subscriptions():
    """Синхронизация подписок между биллингом и Catena"""

    # 1. Получить активные подписки из биллинга
    active_billing_subscriptions = get_active_subscriptions_from_billing()

    # 2. Получить всех абонентов Catena
    response = requests.get(
        f"{CATENA_API_URL}/subscribers",
        headers={"X-Auth-Token": CATENA_API_KEY}
    )
    catena_subscribers = response.json()['subscribers']

    # 3. Сравнить и синхронизировать
    for billing_sub in active_billing_subscriptions:
        phone = billing_sub['phone']
        package_id = get_package_id(billing_sub['tariff'])

        # Найти абонента в Catena
        catena_sub = find_subscriber_by_phone(catena_subscribers, phone)

        if catena_sub:
            # Проверить, есть ли нужная подписка
            if package_id not in catena_sub['packages']:
                # Подписки нет - создаём
                create_subscription(catena_sub['subscriberId'], package_id)
                print(f"Activated: {phone} -> {package_id}")
        else:
            # Абонента нет - создаём с подпиской
            create_subscriber_with_package(phone, package_id)
            print(f"Created subscriber: {phone}")

    # 4. Отключить истекшие подписки
    for catena_sub in catena_subscribers:
        phone = f"+{catena_sub['phoneCountryCode']}{catena_sub['phone']}"

        for package_id in catena_sub['packages']:
            if not has_active_billing_subscription(phone, package_id):
                # В биллинге подписки нет - удаляем из Catena
                delete_subscription(catena_sub['subscriberId'], package_id)
                print(f"Deactivated: {phone} -> {package_id}")

def create_subscription(subscriber_id, package_id):
    requests.post(
        f"{CATENA_API_URL}/packages-subscribers",
        headers={"X-Auth-Token": CATENA_API_KEY},
        json={
            "subscriberId": subscriber_id,
            "packageId": package_id
        }
    )

def delete_subscription(subscriber_id, package_id):
    requests.delete(
        f"{CATENA_API_URL}/packages-subscribers",
        headers={"X-Auth-Token": CATENA_API_KEY},
        json={
            "subscriberId": subscriber_id,
            "packageId": package_id
        }
    )

# Запускать эту функцию по расписанию (например, каждый час)
if __name__ == '__main__':
    sync_subscriptions()

Пример 3: Пробный период

Автоматическое предоставление пробного периода новым абонентам:

import requests
from datetime import datetime, timedelta

def activate_trial_subscription(phone, trial_days=7):
    """Активировать пробную подписку на N дней"""

    # 1. Создать или получить абонента
    subscriber_id = get_or_create_subscriber(phone)

    # 2. Подключить пробный пакет
    trial_package_id = "trialKl9SW3AAAE."

    response = requests.post(
        f"{CATENA_API_URL}/packages-subscribers",
        headers={"X-Auth-Token": CATENA_API_KEY},
        json={
            "subscriberId": subscriber_id,
            "packageId": trial_package_id
        }
    )

    if response.status_code == 200:
        # 3. Запланировать автоматическое отключение
        schedule_subscription_cancellation(
            subscriber_id,
            trial_package_id,
            datetime.now() + timedelta(days=trial_days)
        )

        return {
            "success": True,
            "message": f"Trial activated for {trial_days} days",
            "expires_at": (datetime.now() + timedelta(days=trial_days)).isoformat()
        }

    return {"success": False, "error": response.text}

def schedule_subscription_cancellation(subscriber_id, package_id, cancel_date):
    """Запланировать отключение подписки"""
    # Сохранить в базу задач или использовать планировщик
    # Например, Celery, APScheduler, или cron job
    pass

Типичные сценарии использования

Подписка с автопродлением

Задача: Реализовать месячную подписку с автоматическим продлением

Решение:

  1. Биллинг списывает оплату каждый месяц
  2. При успешном списании биллинг проверяет наличие подписки в Catena
  3. Если подписка есть — ничего не делать (она уже активна)
  4. Если подписки нет — создать её через API
  5. При неудачном списании — удалить подписку через API
def process_monthly_renewal(user_id, package_name):
    """Обработка ежемесячного продления"""

    # Попытка списания
    payment_success = billing_charge(user_id, get_package_price(package_name))

    subscriber_id = get_subscriber_id_by_user(user_id)
    package_id = get_package_id(package_name)

    if payment_success:
        # Платёж успешен - убедиться что подписка активна
        ensure_subscription_active(subscriber_id, package_id)
    else:
        # Платёж не прошёл - отключить подписку
        deactivate_subscription(subscriber_id, package_id)
        send_notification(user_id, "payment_failed")

Семейная подписка

Задача: Один платёж — доступ для нескольких абонентов (семейный аккаунт)

Решение:

  1. В биллинге создать семейный тариф
  2. При оплате подключить пакет всем абонентам семьи
  3. Хранить связь между абонентами в биллинге
def activate_family_subscription(family_id, package_name):
    """Активировать семейную подписку"""

    package_id = get_package_id(package_name)

    # Получить всех членов семьи из биллинга
    family_members = get_family_members(family_id)

    for member in family_members:
        subscriber_id = get_subscriber_id(member['phone'])

        # Подключить пакет каждому
        requests.post(
            f"{CATENA_API_URL}/packages-subscribers",
            headers={"X-Auth-Token": CATENA_API_KEY},
            json={
                "subscriberId": subscriber_id,
                "packageId": package_id
            }
        )

    return {"activated": len(family_members)}

Временная акция

Задача: Дать доступ к премиум каналам на выходные

Решение:

  1. В пятницу вечером подключить промо-пакет всем активным абонентам
  2. В понедельник утром отключить промо-пакет
#!/bin/bash
# friday-promo.sh - запускается по cron в пятницу в 18:00

PROMO_PACKAGE_ID="weekendKl9SW3AAAE."

# Получить всех абонентов
SUBSCRIBERS=$(curl -s -X GET "$CATENA_API_URL/subscribers" \
  -H "X-Auth-Token: $CATENA_API_KEY" \
  | jq -r '.subscribers[].subscriberId')

# Подключить промо-пакет каждому
for SUBSCRIBER_ID in $SUBSCRIBERS; do
  curl -X POST "$CATENA_API_URL/packages-subscribers" \
    -H "X-Auth-Token: $CATENA_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{
      \"subscriberId\": \"$SUBSCRIBER_ID\",
      \"packageId\": \"$PROMO_PACKAGE_ID\"
    }"
done

echo "Promo activated for $(echo "$SUBSCRIBERS" | wc -l) subscribers"
#!/bin/bash
# monday-cleanup.sh - запускается по cron в понедельник в 06:00

PROMO_PACKAGE_ID="weekendKl9SW3AAAE."

SUBSCRIBERS=$(curl -s -X GET "$CATENA_API_URL/subscribers" \
  -H "X-Auth-Token: $CATENA_API_KEY" \
  | jq -r '.subscribers[] | select(.packages[] | contains("'$PROMO_PACKAGE_ID'")) | .subscriberId')

for SUBSCRIBER_ID in $SUBSCRIBERS; do
  curl -X DELETE "$CATENA_API_URL/packages-subscribers" \
    -H "X-Auth-Token: $CATENA_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{
      \"subscriberId\": \"$SUBSCRIBER_ID\",
      \"packageId\": \"$PROMO_PACKAGE_ID\"
    }"
done

echo "Promo deactivated for $(echo "$SUBSCRIBERS" | wc -l) subscribers"

Понижение тарифа

Задача: Абонент переходит с премиум на базовый тариф

Решение:

def downgrade_subscription(subscriber_id, from_package, to_package):
    """Понизить тариф абонента"""

    from_package_id = get_package_id(from_package)
    to_package_id = get_package_id(to_package)

    # 1. Отключить премиум пакет
    requests.delete(
        f"{CATENA_API_URL}/packages-subscribers",
        headers={"X-Auth-Token": CATENA_API_KEY},
        json={
            "subscriberId": subscriber_id,
            "packageId": from_package_id
        }
    )

    # 2. Подключить базовый пакет
    requests.post(
        f"{CATENA_API_URL}/packages-subscribers",
        headers={"X-Auth-Token": CATENA_API_KEY},
        json={
            "subscriberId": subscriber_id,
            "packageId": to_package_id
        }
    )

    # 3. Записать в биллинг
    billing_record_downgrade(subscriber_id, from_package, to_package)

    return {"success": True, "new_package": to_package}

Лучшие практики

Проектирование пакетов

Рекомендации по структуре пакетов:

  • Базовый пакет — минимальный набор каналов для всех
  • Тематические пакеты — спорт, кино, детские, новости
  • Премиум пакеты — эксклюзивный контент, HD/4K каналы
  • Комбо-пакеты — несколько тематик в одном (экономия для абонента)

Избегайте:

  • Слишком много мелких пакетов — усложняет выбор
  • Дублирование каналов между пакетами — путаница в биллинге
  • Пересечения пакетов без логики — один канал в 5 разных пакетах

Обработка ошибок

При интеграции с биллингом:

def safe_create_subscription(subscriber_id, package_id, retry_count=3):
    """Создание подписки с повторными попытками"""

    for attempt in range(retry_count):
        try:
            response = requests.post(
                f"{CATENA_API_URL}/packages-subscribers",
                headers={"X-Auth-Token": CATENA_API_KEY},
                json={
                    "subscriberId": subscriber_id,
                    "packageId": package_id
                },
                timeout=10
            )

            if response.status_code == 200:
                return {"success": True}
            elif response.status_code == 409:
                # Подписка уже существует - это OK
                return {"success": True, "already_exists": True}
            else:
                # Другая ошибка
                error_msg = response.json().get('message', 'Unknown error')
                log_error(f"Failed to create subscription: {error_msg}")

        except requests.exceptions.Timeout:
            log_warning(f"Timeout on attempt {attempt + 1}")
            if attempt < retry_count - 1:
                time.sleep(2 ** attempt)  # Exponential backoff
            continue
        except Exception as e:
            log_error(f"Unexpected error: {str(e)}")
            break

    # Все попытки неудачны - сохранить для ручной обработки
    save_failed_operation("create_subscription", subscriber_id, package_id)
    return {"success": False, "error": "Failed after retries"}

Синхронизация состояния

Регулярная сверка данных:

def audit_subscriptions():
    """Проверка согласованности подписок между системами"""

    discrepancies = []

    # Получить данные из обеих систем
    billing_subscriptions = get_billing_subscriptions()
    catena_subscriptions = get_catena_subscriptions()

    # Найти расхождения
    for billing_sub in billing_subscriptions:
        if not exists_in_catena(billing_sub, catena_subscriptions):
            discrepancies.append({
                "type": "missing_in_catena",
                "subscriber": billing_sub['phone'],
                "package": billing_sub['package']
            })

    for catena_sub in catena_subscriptions:
        if not exists_in_billing(catena_sub, billing_subscriptions):
            discrepancies.append({
                "type": "missing_in_billing",
                "subscriber": catena_sub['phone'],
                "package": catena_sub['package']
            })

    if discrepancies:
        # Отправить уведомление администратору
        send_audit_report(discrepancies)

        # Опционально: автоматически исправить
        auto_fix_discrepancies(discrepancies)

    return discrepancies

Логирование и мониторинг

Что логировать:

  • Все создания и удаления подписок
  • Ошибки при вызове API
  • Время отклика API Catena
  • Несоответствия между биллингом и Catena

Метрики для отслеживания:

import prometheus_client as prom

# Метрики Prometheus
subscription_creations = prom.Counter(
    'catena_subscription_creations_total',
    'Total number of subscription creations',
    ['package_name', 'status']
)

subscription_deletions = prom.Counter(
    'catena_subscription_deletions_total',
    'Total number of subscription deletions',
    ['package_name', 'status']
)

api_latency = prom.Histogram(
    'catena_api_latency_seconds',
    'Latency of Catena API calls',
    ['endpoint', 'method']
)

def monitored_create_subscription(subscriber_id, package_id):
    """Создание подписки с мониторингом"""

    package_name = get_package_name(package_id)

    with api_latency.labels('/packages-subscribers', 'POST').time():
        try:
            response = requests.post(...)

            if response.status_code == 200:
                subscription_creations.labels(package_name, 'success').inc()
                return {"success": True}
            else:
                subscription_creations.labels(package_name, 'error').inc()
                return {"success": False}

        except Exception as e:
            subscription_creations.labels(package_name, 'exception').inc()
            raise

Уведомления абонентам

Когда отправлять уведомления:

  1. При активации подписки — "Добро пожаловать! Теперь доступны каналы: ..."
  2. За 3 дня до окончания — "Ваша подписка истекает через 3 дня"
  3. При продлении — "Подписка продлена до ..."
  4. При отключении — "Подписка отключена. Для продления..."
  5. При ошибке оплаты — "Не удалось списать оплату. Проверьте..."
def notify_subscription_activated(subscriber_id, package_name):
    """Уведомление об активации подписки"""

    subscriber = get_subscriber(subscriber_id)
    phone = f"+{subscriber['phoneCountryCode']}{subscriber['phone']}"

    # Получить список каналов пакета
    package = get_package(get_package_id(package_name))
    channels = ", ".join(package['channels'][:5])  # Первые 5 каналов

    message = f"""
    🎉 Подписка активирована!

    Пакет: {package_name}
    Доступные каналы: {channels} и другие

    Приятного просмотра!
    """

    send_sms(phone, message)

Устранение проблем

Подписка не создаётся

Возможные причины:

  • Неверный subscriberId или packageId
  • Абонент и пакет из разных порталов
  • Подписка уже существует
  • Проблемы с авторизацией API

Решение:

  1. Проверьте существование абонента: GET /subscribers/{id}
  2. Проверьте существование пакета: GET /packages/{id}
  3. Убедитесь, что portalId совпадает
  4. Проверьте текущие подписки абонента
  5. Проверьте валидность API ключа

Абонент не видит каналы после создания подписки

Возможные причины:

  • Пакет не содержит каналов
  • Приложение не обновило список каналов
  • Проблемы со streaming-сервером

Решение:

  1. Проверьте содержимое пакета: GET /packages/{id}
  2. Убедитесь, что в пакете есть каналы
  3. Попросите абонента перезапустить приложение
  4. Проверьте playback_token абонента
  5. Проверьте логи streaming-сервера

Подписка не удаляется

Возможные причины:

  • Подписки не существует (уже удалена)
  • Неверные параметры запроса
  • Это бесплатный пакет портала (удалить нельзя)

Решение:

  1. Проверьте текущие подписки абонента
  2. Убедитесь, что это не бесплатный пакет портала
  3. Проверьте правильность subscriberId и packageId
  4. Посмотрите журнал операций для этого абонента

Расхождения между биллингом и Catena

Проблема: В биллинге подписка активна, в Catena — нет (или наоборот)

Решение:

  1. Реализуйте регулярную синхронизацию (каждые 15-60 минут)
  2. Используйте журнал операций для выявления проблем
  3. При расхождении приоритет имеет биллинг (источник истины)
  4. Логируйте все изменения для анализа
def fix_sync_issue(subscriber_id):
    """Исправить рассинхронизацию для абонента"""

    # 1. Получить "правду" из биллинга
    billing_packages = get_billing_packages(subscriber_id)

    # 2. Получить текущее состояние в Catena
    subscriber = get_catena_subscriber(subscriber_id)
    catena_packages = subscriber['packages']

    # 3. Синхронизировать
    for package_id in billing_packages:
        if package_id not in catena_packages:
            # Должна быть, но нет - добавляем
            create_subscription(subscriber_id, package_id)
            log_info(f"Fixed: added {package_id} to {subscriber_id}")

    for package_id in catena_packages:
        if package_id not in billing_packages:
            # Есть, но не должна быть - удаляем
            delete_subscription(subscriber_id, package_id)
            log_info(f"Fixed: removed {package_id} from {subscriber_id}")

См. также