fix: send subscription Stars invoice directly
This commit is contained in:
1 parent
6dd685a6a5
commit
aaea5b6f58
7 files changed
+10
-62
No files matched your search
@@ -21,7 +21,7 @@ Telegram-бот, который по запросу пользователя г
|
||||
- **Главное меню** — внутреннее состояние после `/start` или выхода из режима; пользователь не выбирает его как отдельный режим.
|
||||
- **Обучение** — принимает скриншоты, накапливает образцы, показывает стиль (`/style`); `/clear` очищает профиль и историю, не затрагивая подписку или триал.
|
||||
- **Генерация** — создаёт и редактирует посты, предлагает идеи (`/post`, `/idea`); генерация требует активного доступа.
|
||||
- **Подписка** — при входе показывает активный период и остаток дней; inline-кнопки продления и оплаты ведут к Stars-инвойсу (`/balance`, `/subscribe`).
|
||||
- **Подписка** — при входе показывает активный период и остаток дней; inline-кнопка продления сразу открывает Stars-инвойс (`/balance`, `/subscribe`).
|
||||
- **Помощь** — справка и навигация по её разделам (`/help`).
|
||||
|
||||
`/start` показывает ровно одно welcome-сообщение без автоматического вызова помощи. При входе в любой режим показывается короткий onboarding. Главное меню содержит только четыре пользовательских режима; внутри каждого режима клавиатура состоит из кнопки «Главное меню». Команды `/training`, `/generate`, `/subscription`, `/help` доступны из любого режима; профильные действия — только в своём режиме.
|
||||
@@ -48,7 +48,7 @@ Telegram-бот, который по запросу пользователя г
|
||||
- Таблица `subscriptions`: `telegram_user_id`, `trial_started_at`, `last_paid_at`, `is_active`, `monthly_days`, `updated_at`.
|
||||
- `/start` создаёт запись триала (`ensure_subscription`), если её нет.
|
||||
- `/subscribe` отправляет инвойс на 100 звёзд; подписка продлевается только обработчиком `successful_payment` после подтверждённой оплаты.
|
||||
- Вход в «Подписка» сразу показывает активный тип периода и сколько дней осталось; inline-кнопка «Продлить подписку» открывает сообщение для покупки.
|
||||
- Вход в «Подписка» сразу показывает активный тип периода и сколько дней осталось; inline-кнопка «Продлить подписку» сразу открывает Stars-инвойс.
|
||||
- `/balance` показывает статус: триал активен (сколько дней осталось) / подписка активна (сколько дней) / период истёк.
|
||||
- Генерация поста и идеи блокируется, если подписка не активна (`_check_subscription`); переходы между режимами доступны всегда.
|
||||
- `/balance` и `/subscribe` доступны в режиме «Подписка»; `/clear` доступна только в режиме «Обучение».
|
||||
|
||||
@@ -44,7 +44,7 @@ Telegram-бот для генерации постов для соцсетей.
|
||||
«Главное меню» — внутренняя точка входа после `/start` и выхода из режима, а не отдельный режим.
|
||||
Внутри любого режима reply-клавиатура содержит только «🏠 Главное меню». При входе показывается короткий
|
||||
onboarding. В «Подписке» сразу отображаются тип активного периода и оставшиеся дни; inline-кнопка
|
||||
«Продлить подписку» открывает предложение, а кнопка оплаты создаёт Stars-инвойс.
|
||||
«Продлить подписку» сразу открывает Stars-инвойс на оплату.
|
||||
|
||||
Команды перехода: `/start`, `/training`, `/generate`, `/subscription`, `/help`; `/start` возвращает
|
||||
в главное меню. Команды `/style` и `/clear` доступны только в обучении; `/post` и `/idea` — в генерации;
|
||||
@@ -65,7 +65,7 @@ onboarding. В «Подписке» сразу отображаются тип
|
||||
| Придумать идею | В режиме «Генерация» пишет «предложи тему» или `/idea` | Одна идея одним сообщением |
|
||||
| Настроить стиль | В режиме «Обучение» присылает скриншоты постов | Бот распознаёт тексты, накапливает их и обновляет ToV |
|
||||
| Голосом | В режиме «Генерация» отправляет голосовое | STT расшифровывает → обычный текстовый запрос |
|
||||
| Узнать статус / продлить | Входит в режим «Подписка», затем нажимает «Продлить подписку» и «Оплатить» | Текущий тип периода, остаток дней и Stars-инвойс на 100 звёзд |
|
||||
| Узнать статус / продлить | Входит в режим «Подписка» и нажимает «Продлить подписку» | Текущий тип периода, остаток дней и Stars-инвойс на 100 звёзд |
|
||||
| Очистить данные | В режиме «Обучение» вызывает `/clear` | Очищает стиль и историю, не меняя подписку или триал |
|
||||
|
||||
### Индикаторы и обратная связь
|
||||
|
||||
+1
-3
@@ -23,9 +23,7 @@ flowchart TB
|
||||
SELECT --> SUBS
|
||||
SUBS --> STATUS[Показать активный период<br/>и остаток дней]
|
||||
STATUS --> RENEW[Inline: «Продлить подписку»]
|
||||
RENEW --> OFFER[Сообщение с предложением оплаты]
|
||||
OFFER --> PAY[Inline: «Оплатить»]
|
||||
PAY --> INVOICE[Stars-инвойс]
|
||||
RENEW --> INVOICE[Сразу открыть Stars-инвойс]
|
||||
INVOICE --> SUCCESS[Оплата подтверждена<br/>successful_payment]
|
||||
|
||||
SELECT --> HELP[Помощь<br/>короткий onboarding и разделы]
|
||||
|
||||
@@ -346,17 +346,6 @@ def subscription_renew_keyboard() -> InlineKeyboardMarkup:
|
||||
)
|
||||
|
||||
|
||||
def subscription_purchase_keyboard() -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[[
|
||||
InlineKeyboardButton(
|
||||
text=f"Оплатить {SUBSCRIPTION_PRICE_STARS} ⭐",
|
||||
callback_data="subscription:buy",
|
||||
)
|
||||
]]
|
||||
)
|
||||
|
||||
|
||||
async def balance_command(message: Message, state: FSMContext) -> None:
|
||||
if not await require_mode(message, state, "subscription"):
|
||||
return
|
||||
@@ -412,23 +401,6 @@ async def _send_subscription_invoice(message: Message, user_id: int) -> None:
|
||||
|
||||
@router.callback_query(F.data == "subscription:renew")
|
||||
async def subscription_renew(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
if await current_mode(state) != "subscription":
|
||||
await callback.answer("Открой режим «Подписка» из главного меню.", show_alert=True)
|
||||
return
|
||||
message = callback.message
|
||||
if not isinstance(message, Message):
|
||||
await callback.answer("Открой режим «Подписка» ещё раз.", show_alert=True)
|
||||
return
|
||||
await callback.answer()
|
||||
await message.answer(
|
||||
f"Продление подписки на 30 дней — {SUBSCRIPTION_PRICE_STARS} ⭐. "
|
||||
"Нажми кнопку, чтобы открыть счёт.",
|
||||
reply_markup=subscription_purchase_keyboard(),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "subscription:buy")
|
||||
async def subscription_buy(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
if await current_mode(state) != "subscription":
|
||||
await callback.answer("Открой режим «Подписка» из главного меню.", show_alert=True)
|
||||
return
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ HELP_SECTIONS: dict[str, tuple[str, str]] = {
|
||||
<li><code>/balance</code> — узнать статус: сколько осталось дней триала или подписки.</li>
|
||||
<li><code>/subscribe</code> — оплатить подписку на 30 дней.</li>
|
||||
</ul>
|
||||
<p>При входе в режим «Подписка» бот сразу показывает активный период и сколько дней осталось. Нажми «Продлить подписку», затем «Оплатить», чтобы открыть счёт. Когда период закончится, генерация постов и идей станет недоступна.</p>""",
|
||||
<p>При входе в режим «Подписка» бот сразу показывает активный период и сколько дней осталось. Нажми «Продлить подписку», чтобы сразу открыть счёт в звёздах. Когда период закончится, генерация постов и идей станет недоступна.</p>""",
|
||||
),
|
||||
"commands": (
|
||||
"⌨️ Команды",
|
||||
|
||||
+3
-25
@@ -173,46 +173,24 @@ class ModeRoutingTests(unittest.IsolatedAsyncioTestCase):
|
||||
"subscription:renew",
|
||||
)
|
||||
|
||||
async def test_renew_callback_opens_purchase_prompt(self):
|
||||
async def test_renew_callback_sends_invoice_without_intermediate_message(self):
|
||||
message_bot = AsyncMock()
|
||||
message = Message(
|
||||
message_id=91,
|
||||
date=datetime.now(),
|
||||
chat=Chat(id=555, type="private"),
|
||||
).as_(message_bot)
|
||||
callback = SimpleNamespace(
|
||||
message=message,
|
||||
answer=AsyncMock(),
|
||||
)
|
||||
await bot.subscription_renew(callback, _StubState("subscription"))
|
||||
|
||||
callback.answer.assert_awaited_once_with()
|
||||
sent_call = message_bot.await_args
|
||||
if sent_call is None:
|
||||
self.fail("purchase prompt was not sent")
|
||||
sent = sent_call.args[0]
|
||||
self.assertIn("Продление подписки", sent.text)
|
||||
self.assertEqual(
|
||||
sent.reply_markup.inline_keyboard[0][0].callback_data,
|
||||
"subscription:buy",
|
||||
)
|
||||
|
||||
async def test_buy_callback_sends_invoice_for_clicking_user(self):
|
||||
message = Message(
|
||||
message_id=92,
|
||||
date=datetime.now(),
|
||||
chat=Chat(id=555, type="private"),
|
||||
).as_(AsyncMock())
|
||||
callback = SimpleNamespace(
|
||||
message=message,
|
||||
from_user=SimpleNamespace(id=42),
|
||||
answer=AsyncMock(),
|
||||
)
|
||||
with patch.object(bot, "_send_subscription_invoice", new=AsyncMock()) as send_invoice:
|
||||
await bot.subscription_buy(callback, _StubState("subscription"))
|
||||
await bot.subscription_renew(callback, _StubState("subscription"))
|
||||
|
||||
callback.answer.assert_awaited_once_with()
|
||||
send_invoice.assert_awaited_once_with(message, 42)
|
||||
message_bot.assert_not_awaited()
|
||||
|
||||
async def test_idea_command_generates_only_in_generation_mode(self):
|
||||
message = _StubMessage("/idea")
|
||||
|
||||
Reference in new issue
Block a user