feat(autorenewal): implement automatic subscription renewal system
This commit is contained in:
@@ -166,9 +166,6 @@ async def toggle_autorenewal(
|
||||
cb: CallbackQuery, state: FSMContext, session: AsyncSession, deps: DependenciesDTO
|
||||
):
|
||||
await state.clear()
|
||||
await cb.answer(NOTHING_PLACEHOLDER + " В разработке.")
|
||||
return
|
||||
|
||||
await cb.message.edit_text(GENERAL_PROCESSING)
|
||||
sub = await deps.subscriptions_repository.get_subscription_by_user_id(session, cb.from_user.id)
|
||||
|
||||
@@ -186,7 +183,24 @@ async def toggle_autorenewal(
|
||||
session, cb.from_user.id, deps.rw_sdk
|
||||
)
|
||||
|
||||
await cb.answer(GENERAL_SUCCESS)
|
||||
status_text = "включено" if new_autorenew else "отключено"
|
||||
await cb.answer(f"Автопродление {status_text}")
|
||||
await cb.message.edit_text(
|
||||
deps.user_service.format_subscription_message(
|
||||
rw_user, link=user.subscription_link, autorenew=sub.autorenew
|
||||
),
|
||||
reply_markup=subscription_controls(
|
||||
show_autorenew=bool(rw_user), current_autorenewal_status=sub.autorenew
|
||||
),
|
||||
)
|
||||
|
||||
rw_user = await get_user_by_telegram_id(deps.rw_sdk, cb.from_user.id)
|
||||
user: User | None = await deps.user_service.update_user_link(
|
||||
session, cb.from_user.id, deps.rw_sdk
|
||||
)
|
||||
|
||||
status_text = "включено" if new_autorenew else "отключено"
|
||||
await cb.answer(f"Автопродление {status_text}")
|
||||
await cb.message.edit_text(
|
||||
deps.user_service.format_subscription_message(
|
||||
rw_user, link=user.subscription_link, autorenew=sub.autorenew
|
||||
|
||||
29
bot/main.py
29
bot/main.py
@@ -8,6 +8,7 @@ from remnawave import RemnawaveSDK
|
||||
|
||||
from bot.handlers import admin_routers, client_routers, system_routers
|
||||
from bot.middlewares import DIMiddleware
|
||||
from bot.scheduler import start_scheduler
|
||||
from config import settings
|
||||
from db.session import async_session
|
||||
from misc.pally import PallyClient
|
||||
@@ -16,6 +17,7 @@ from repositories.subscriptions import SubscriptionRepository
|
||||
from repositories.users import UserRepository
|
||||
from schemas.di import DependenciesDTO
|
||||
from services.billing_service import BillingService
|
||||
from services.renewal_service import RenewalService
|
||||
from services.user_service import UserService
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
@@ -40,6 +42,17 @@ async def main():
|
||||
token=settings.remnawave_token,
|
||||
)
|
||||
|
||||
aiohttp_session = AiohttpSession(proxy=settings.proxy) if settings.proxy else None
|
||||
default = DefaultBotProperties(parse_mode="HTML")
|
||||
bot = Bot(token=settings.bot_token, default=default, session=aiohttp_session)
|
||||
|
||||
renewal_service = RenewalService(
|
||||
subscription_repository=subscriptions_repository,
|
||||
user_repository=user_repository,
|
||||
rw_sdk=rw_sdk,
|
||||
bot=bot,
|
||||
)
|
||||
|
||||
deps = DependenciesDTO(
|
||||
user_repository=user_repository,
|
||||
user_service=user_service,
|
||||
@@ -48,13 +61,10 @@ async def main():
|
||||
bills_repository=bills_repository,
|
||||
billing_service=billing_service,
|
||||
subscriptions_repository=subscriptions_repository,
|
||||
renewal_service=renewal_service,
|
||||
)
|
||||
dp.update.middleware.register(DIMiddleware(deps, async_session))
|
||||
|
||||
aiohttp_session = AiohttpSession(proxy=settings.proxy) if settings.proxy else None
|
||||
default = DefaultBotProperties(parse_mode="HTML")
|
||||
bot = Bot(token=settings.bot_token, default=default, session=aiohttp_session)
|
||||
|
||||
for r in admin_routers:
|
||||
dp.include_router(r)
|
||||
for r in client_routers:
|
||||
@@ -62,7 +72,16 @@ async def main():
|
||||
for r in system_routers:
|
||||
dp.include_router(r)
|
||||
|
||||
await dp.start_polling(bot)
|
||||
scheduler_task = asyncio.create_task(start_scheduler(renewal_service, async_session))
|
||||
|
||||
try:
|
||||
await dp.start_polling(bot)
|
||||
finally:
|
||||
scheduler_task.cancel()
|
||||
try:
|
||||
await scheduler_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
29
bot/scheduler.py
Normal file
29
bot/scheduler.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from services.renewal_service import RenewalService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def run_autorenewal(renewal_service: RenewalService, session_factory: async_sessionmaker):
|
||||
async with session_factory() as session:
|
||||
try:
|
||||
results = await renewal_service.process_autorenewals(session)
|
||||
logger.info("Autorenewal job completed: %s", results)
|
||||
except Exception:
|
||||
logger.exception("Autorenewal job failed")
|
||||
await session.rollback()
|
||||
|
||||
|
||||
async def start_scheduler(
|
||||
renewal_service: RenewalService,
|
||||
session_factory: async_sessionmaker,
|
||||
interval_hours: int = 24,
|
||||
):
|
||||
logger.info("Starting autorenewal scheduler with interval %dh", interval_hours)
|
||||
while True:
|
||||
await run_autorenewal(renewal_service, session_factory)
|
||||
await asyncio.sleep(interval_hours * 3600)
|
||||
@@ -219,3 +219,12 @@ ADM_VERIFY_PROMOTION = (
|
||||
"<b>❓ Отправить рассылку? <i>Это действие будет невозможно отменить.</i></b>"
|
||||
)
|
||||
SUCCESSFULLY_SENT_PROMO = "<b>💚 Успешно отправлена рассылка.</b>\n<i>⚡ Успешных отправок: <code>{successful}</code>.</i>"
|
||||
|
||||
AUTORENEWAL_INSUFFICIENT_FUNDS = (
|
||||
"<b>⚠️ Не удалось продлить подписку автоматически.</b>\n"
|
||||
"Недостаточно средств на балансе (нужно <code>{price}₽</code>, <code>доступно {balance}₽</code>). Автопродление отключено."
|
||||
)
|
||||
AUTORENEWAL_SUCCESS = (
|
||||
"<b>✅ Подписка продлена автоматически. Списано <code>{price}₽</code>.</b>"
|
||||
"Новая дата окончания: <code>{end_date}</code>."
|
||||
)
|
||||
Reference in New Issue
Block a user