Skip to content

[epic] D1+ push для юзеров начавших урок но не вернувшихся #940

@Undermove

Description

@Undermove

D1+ push для юзеров начавших урок но не вернувшихся

Source: аналитика рекламной кампании 13 мая 2026 (~/repos/ai-hub/analytics/tralebot-2026-05-13-ad-campaign.md)

Goal: Из юзеров когорты 13 мая 53 человека прошли хотя бы один шаг урока, и 21% из них всё ещё активны на D6+. Это «живая» аудитория для удержания. Но из 39 «вернулись хотя бы раз» к D6+ доходит ~11 — половина теряется между D2 и D6. Цель эпика: отлавливать юзеров которые начали учиться но не вернулись на следующий день, и звать обратно через пуш в боте. КРИТИЧНО: пуш должен идти ТОЛЬКО тем, кто прошёл хотя бы один шаг урока — для незаведённых юзеров пуш бесполезен (D6+ retention 0% подтверждает).

Out of scope: push для юзеров не сделавших первый шаг (по данным они не возвращаются, нет смысла); push для трёхдневного отсутствия (V2 — пока только D1+); push для специфических модулей или контента (V2); адаптивные тексты под уровень (V2).

expected_impact: $30/мес (предположение: 10 lesson-starter-юзеров/неделя × +20% return rate × 5% paid × $1.50 = немного, но накопительный эффект через 3-6 месяцев — заметный).

Acceptance criteria (BDD)

  • Scenario 1 — пуш приходит когда юзер начал урок но не зашёл вчера

    • Given: юзер X с CompletedLessonsJson != {} (прошёл хотя бы один шаг), LastPlayedAtUtc < вчера 06:00 UTC, IsActive = true, NotificationsEnabled = true
    • When: Cron NotificationDispatcher срабатывает в 10:00 UTC (= 14:00 по Батуми, утро для большинства русскоязычных юзеров)
    • Then: Telegram-сообщение от бота с текстом «Бомбора по тебе скучает 🐶 Продолжишь алфавит сегодня?» (имя модуля подставляется из последнего сыгранного), WebApp-кнопка ведёт прямо в следующий незавершённый урок этого модуля; в записи NotificationTrigger source=daily_return фиксируется LastSentAt
  • Scenario 2 — пуш не дублируется в тот же день

    • Given: пуш уже был отправлен сегодня (LastSentAt = сегодня UTC)
    • When: Cron срабатывает повторно в тот же день
    • Then: второе сообщение НЕ отправляется
  • Scenario 3 — пуш НЕ идёт если юзер не сделал ни одного шага урока

    • Given: юзер Y с CompletedLessonsJson = {} (только выбрал level), LastPlayedAtUtc < вчера, IsActive = true
    • When: Cron срабатывает
    • Then: сообщение НЕ отправляется этому юзеру; в коде явный гейт user.CompletedLessons != empty
  • Scenario 4 — кулдаун 3 дня для одного и того же юзера

    • Given: юзеру X отправили пуш 3 дня назад, он не открывал мини-апп с тех пор
    • When: Cron срабатывает на 4-й день
    • Then: второй пуш не отправляется (3-дневный cooldown истёк, но фактический cooldown 7 дней чтобы не задалбывать неактивных); в payload в логе видно «skipped due to cooldown»
  • Scenario 5 — глубокая ссылка ведёт точно в нужный урок

    • Given: юзер кликает WebApp-кнопку в пуш-сообщении
    • When: мини-апп открывается
    • Then: сразу показывается экран Practice или LessonTheory нужного урока (по deep-link параметру); не Dashboard, не модуль-выбор; trackEvent push_clicked фиксируется
  • Scenario 6 — opt-out через /notifications off уже работает

    • Given: юзер ранее отключил уведомления (NotificationsEnabled = false)
    • When: Cron срабатывает
    • Then: пуш не отправляется; ничего не пишется в LastSentAt
  • Scenario 7 (negative) — pro юзер с активным sub не получает «come back» пушей (или получает с другим тоном)

    • Given: Pro-юзер с активной подпиской не открывал мини-апп 2 дня
    • When: Cron срабатывает
    • Then: в V1 идёт тот же пуш (это не критично, не блокер); V2 — отдельный тон для Pro («твоя подписка работает, не теряй темп»)
  • Scenario 8 — A/B по двум текстам (опционально, если NotificationDispatcher уже поддерживает варианты)

    • Given: два варианта текста A («Бомбора скучает») и B («Алфавит ждёт продолжения»)
    • When: пуш отправляется юзеру
    • Then: случайный выбор A/B, payload содержит variant; через 2 недели можно сравнить open rate (нужен event push_clicked из эпика A)

Technical constraints

  • Зависит от наличия NotificationDispatcher (epic-894). Если этот сервис уже работает — расширяем новым типом триггера daily_return. Если нет — этот эпик включает в себя минимальную имплементацию.
  • Cron-окно: один раз в день, 10:00 UTC (14:00 Батуми). Достаточно для V1.
  • Deep-link в Practice: формат WebAppData с payload {moduleId, lessonId}; нужно расширить router в мини-аппе чтобы он принимал такой initial state.
  • Кулдаун 7 дней на одного юзера — иначе риск превратить бот в спам и получить блок от Telegram.

Existing artefacts

  • Notification infrastructure: ожидается из epic-894 (Контекстные пуш-нотификации). Если он не завершён — этот эпик блокируется или требует параллельной имплементации.
  • StartCommand handles deep links: src/Infrastructure/Telegram/BotCommands/StartCommand.cs
  • analytics rationale: ~/repos/ai-hub/analytics/tralebot-2026-05-13-ad-campaign.md

Зависимости и порядок

  • ЗАВИСИТ от эпика B (first-lesson UX fix). Без него у нас мало lesson-starters и пуш бесполезен.
  • ЗАВИСИТ или соответствует epic-894 (если он уже даёт NotificationDispatcher — расширяем; если нет — делаем минимальную инфраструктуру здесь).
  • Эпик A (event tracking) желателен для измерения, но не критичен.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Critical priority (launch-blocking or needs-fix)epicEpic: a coherent unit of nightly work, with BDD scenariosepic-draftEpic just created, awaiting reviewer comments

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions