Implement buy flow and migrate DTOs to schemas

Move DTO definitions from dto/ to schemas/ and update imports.
Add buy handler with FSM state management for device selection,
whitelist toggling, and duration choice. Include pricing logic,
inline keyboards, and new configuration fields.
This commit is contained in:
2026-04-08 19:34:25 +07:00
parent fc45816eba
commit 930fabc028
17 changed files with 291 additions and 50 deletions

View File

@@ -1,4 +1,5 @@
from .menus import router as menus_router
from .support import router as support_router
from .buy import router as buy_router
routers = [menus_router, support_router]
routers = [menus_router, support_router, buy_router]

View File

@@ -1,10 +1,153 @@
from aiogram import Router
from typing import Optional
from aiogram import Router, F
from aiogram.types import CallbackQuery
from aiogram.fsm.context import FSMContext
from dto.di import DependenciesDTO
from keyboards.client import devices_selector, duration_selector
from misc.utils import calculate_price, convert_int_to_duration
from schemas.subscriptions import SubscriptionPlan
from states.buy import SubscriptionStorage
from texts import (
CALLBACK_FALLBACK,
CONTEXT_REINITIATED,
NOTHING_PLACEHOLDER,
SUBSCRIPTION_DEVICE_SELECTOR,
SUBSCRIPTION_DURATION_SELECTOR,
)
from config import settings
router = Router()
# @router.callback_query()
@router.callback_query(F.data == "buy")
async def buy_init(cb: CallbackQuery, state: FSMContext):
await state.clear()
ctx = SubscriptionPlan()
await cb.message.edit_text(
SUBSCRIPTION_DEVICE_SELECTOR.format(price=calculate_price(ctx)),
reply_markup=devices_selector(settings.min_devices),
)
await state.set_state(SubscriptionStorage.devices)
await state.set_data({"ctx": ctx})
@router.callback_query(F.data.startswith("devices:"))
async def device_count_select(cb: CallbackQuery, state: FSMContext):
data = await state.get_data()
await state.clear()
ctx: Optional[SubscriptionPlan] = data.get("ctx", SubscriptionPlan())
cb_data = cb.data.split(":")
devices = int(cb_data[1]) if cb_data[1].isdigit() else settings.min_devices
ctx.devices = devices
ctx.whitelists = ctx.whitelists or devices >= settings.whitelist_device_threshold
await cb.message.edit_text(
SUBSCRIPTION_DEVICE_SELECTOR.format(price=calculate_price(ctx)),
reply_markup=devices_selector(current=ctx.devices, whitelists=ctx.whitelists),
)
await state.set_state(SubscriptionStorage.devices)
await state.set_data({"ctx": ctx})
@router.callback_query(SubscriptionStorage.devices, F.data == "whitelist:toggle")
async def toggle_whitelists(cb: CallbackQuery, state: FSMContext):
data = await state.get_data()
await state.clear()
ctx: Optional[SubscriptionPlan] = data.get("ctx")
if not ctx:
await state.set_state(SubscriptionPlan())
await state.set_data({"ctx": ctx})
await cb.answer(CALLBACK_FALLBACK)
return
ctx.whitelists = (
not ctx.whitelists or ctx.devices >= settings.whitelist_device_threshold
)
try:
await cb.message.edit_reply_markup(
SUBSCRIPTION_DEVICE_SELECTOR.format(price=calculate_price(ctx)),
reply_markup=devices_selector(
current=ctx.devices, whitelists=ctx.whitelists
),
)
except Exception as exc:
if "message is not modified:" in str(exc):
await cb.answer(NOTHING_PLACEHOLDER)
else:
await cb.answer(CALLBACK_FALLBACK)
await state.set_state(SubscriptionStorage.devices)
await state.set_data({"ctx": ctx})
@router.callback_query(SubscriptionStorage.devices, F.data == "confirm")
async def select_duration_init(cb: CallbackQuery, state: FSMContext):
data = await state.get_data()
await state.clear()
ctx: Optional[SubscriptionPlan] = data.get("ctx")
if not ctx:
await state.set_state(SubscriptionPlan())
await state.set_data({"ctx": ctx})
await cb.answer(CALLBACK_FALLBACK)
return
try:
await cb.message.edit_text(
SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)),
reply_markup=duration_selector(
current=ctx.duration, back_cb=f"devices:{ctx.devices}"
),
)
except Exception as exc:
if "message is not modified:" in str(exc):
await cb.answer(NOTHING_PLACEHOLDER)
else:
await cb.answer(CALLBACK_FALLBACK)
raise
await state.set_state(SubscriptionStorage.duration)
await state.set_data({"ctx": ctx})
@router.callback_query(F.data.startswith("duration:"))
async def select_duration(cb: CallbackQuery, state: FSMContext):
data = await state.get_data()
await state.clear()
ctx: Optional[SubscriptionPlan] = data.get("ctx")
if not ctx:
await state.set_state()
await state.set_data({"ctx": ctx})
await cb.answer(CONTEXT_REINITIATED)
ctx = SubscriptionPlan()
cb_data = cb.data.split(":")
duration = int(cb_data[1]) if cb_data[1].isdigit() else 1
ctx.duration = convert_int_to_duration(duration)
try:
await cb.message.edit_text(
SUBSCRIPTION_DURATION_SELECTOR.format(price=calculate_price(ctx)),
reply_markup=duration_selector(
current=ctx.duration, back_cb=f"devices:{ctx.devices}"
),
)
except Exception as exc:
if "message is not modified:" in str(exc):
await cb.answer(NOTHING_PLACEHOLDER)
else:
await cb.answer(CALLBACK_FALLBACK)
raise
await state.set_state(SubscriptionStorage.duration)
await state.set_data({"ctx": ctx})
@router.callback_query(SubscriptionStorage.duration, F.data == "confirm")
async def proceed_to_checkout(cb: CallbackQuery, state: FSMContext):
await cb.answer("yay!!!! fuck off.")
...

View File

@@ -4,7 +4,7 @@ from aiogram.types import CallbackQuery, Message
from aiogram.fsm.context import FSMContext
from sqlalchemy.ext.asyncio import AsyncSession
from dto.di import DependenciesDTO
from schemas.di import DependenciesDTO
from keyboards.client import main_menu
from texts import build_main_menu_text

View File

@@ -6,7 +6,7 @@ from aiogram.types import CallbackQuery, Message
from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from dto.di import DependenciesDTO
from schemas.di import DependenciesDTO
from services.user_service import UserServiceException
from states.support import SupportStorage
from texts import GENERAL_PROCESSING, STANDARD_FALLBACK, SUPPORT_INIT, SUPPORT_MSG_SENT