feat: Implement payment notification system and API structure

- Added notifications for successful payments in `api/core/notifications.py`.
- Created main API entry point in `api/main.py` with FastAPI integration.
- Established routing structure in `api/routes/__init__.py` and `api/routes/pally.py` for handling payment callbacks.
- Developed billing handlers in `bot/handlers/billing.py` and `bot/handlers/buy.py` for subscription management.
- Introduced state management for user interactions in `bot/states/`.
- Created user interface elements in `bot/keyboards/` for navigation and payment processing.
- Set up middleware for dependency injection in `bot/middlewares/di.py`.
- Added support for user commands and interactions in `bot/handlers/common.py` and `bot/handlers/support.py`.
- Implemented logging and error handling throughout the bot's functionality.
- Created entry point script for API server in `entrypoints/api.sh`.
This commit is contained in:
2026-04-24 17:41:42 +07:00
parent d98383285e
commit 049f31118d
37 changed files with 292 additions and 41 deletions

0
api/__init__.py Normal file
View File

18
api/core/deps.py Normal file
View File

@@ -0,0 +1,18 @@
from aiogram import Bot
from fastapi import Request
from db.session import async_session
from schemas.di import APIDependenciesDTO
def get_bot(request: Request) -> Bot:
return request.app.state.bot
async def get_db():
async with async_session() as db:
yield db
def get_deps(request: Request) -> APIDependenciesDTO:
return request.app.state.deps

13
api/core/notifications.py Normal file
View File

@@ -0,0 +1,13 @@
from aiogram import Bot
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
SUCCESSFUL_PAYMENT = "<b>💸 Зачислен платёж на {amount}₽</b>"
close_kb: InlineKeyboardMarkup = InlineKeyboardBuilder(
[[InlineKeyboardButton(text="", callback_data="delete")]]
).as_markup()
async def successful_payment_notification(bot: Bot, user_id: int, amount: int):
await bot.send_message(user_id, SUCCESSFUL_PAYMENT.format(amount=amount), reply_markup=close_kb)

52
api/main.py Normal file
View File

@@ -0,0 +1,52 @@
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from aiogram import Bot
from fastapi import FastAPI
from api.routes import routers
from config import settings
from repositories.bills import BillsRepository
from repositories.users import UserRepository
from schemas.di import APIDependenciesDTO
from services.billing_service import BillingService
from services.user_service import UserService
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator:
# Bot Init
bot = Bot(token=settings.bot_token)
app.state.bot = bot
user_repository = UserRepository()
user_service = UserService(user_repository)
bills_repository = BillsRepository()
bills_service = BillingService(
user_repository=user_repository, bills_repository=bills_repository
)
deps_dto = APIDependenciesDTO(
user_repository=user_repository,
user_service=user_service,
bills_repository=bills_repository,
billing_service=bills_service,
)
app.state.deps = deps_dto
for r in routers:
app.include_router(r)
yield # App's lifespan
await bot.session.close() # D IE!!!!!!!!
app = FastAPI(
title="Malenia API",
description="Payment webhook handler and user balance management",
version="1.0.0",
lifespan=lifespan,
docs_url="",
)

3
api/routes/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .pally import router as pally_router
routers = [pally_router]

78
api/routes/pally.py Normal file
View File

@@ -0,0 +1,78 @@
import hashlib
import logging
import math
from aiogram import Bot
from fastapi import Depends, Form, HTTPException
from fastapi.routing import APIRouter
from sqlalchemy.ext.asyncio import AsyncSession
from api.core.deps import get_bot, get_db, get_deps
from api.core.notifications import successful_payment_notification
from config import settings
from misc.pally import BillStatus
from schemas.di import APIDependenciesDTO
router = APIRouter(prefix="/checkout/pally")
logger = logging.getLogger(__name__)
@router.post("/callback")
async def pally_callback(
id: str = Form(...),
bill_id: str = Form(...),
status: str = Form(...),
req_amount: str = Form(...),
currency: str = Form(...),
order_id: str | None = Form(None),
signature: str = Form(...),
session: AsyncSession = Depends(get_db),
bot: Bot = Depends(get_bot),
deps: APIDependenciesDTO = Depends(get_deps),
):
oid = order_id or ""
raw_string = f"{id}{status}{req_amount}{currency}{oid}{settings.pally_token}"
md5_hash = hashlib.md5(raw_string.encode("utf-8")).hexdigest()
expected_signature = hashlib.sha1(md5_hash.encode("utf-8")).hexdigest()
if signature != expected_signature:
logger.critical("Invalid signature for bill %s", bill_id)
raise HTTPException(403, detail="Invalid signature.")
if status != BillStatus.SUCCESS or not req_amount.isdigit():
logger.info("Bill %s skipped: status=%s, req_amount=%s", bill_id, status, req_amount)
return "OK"
logger.info("Received successfully paid bill %s", bill_id)
if not oid.isdigit():
logger.critical("Invalid order_id format for bill %s", bill_id)
return "OK"
bill = await deps.bills_repository.get_bill_by_id(session, int(oid))
if not bill:
logger.critical("Bill %s is not found in db", bill_id)
return "OK"
try:
amount = int(req_amount)
await deps.user_repository.increase_user_balance(session, bill.creator_id, amount=amount)
await successful_payment_notification(bot, bill.creator_id, amount)
referal = bill.user.referal_id
if referal is not None:
referal_amount = math.floor(amount * (settings.referal_bonus / 100))
await deps.user_repository.increase_user_balance(session, referal, referal_amount)
await successful_payment_notification(bot, referal, referal_amount)
except Exception:
logger.exception(
"Exception caught while processing balances for bill %s (Creator: %s)",
bill_id,
bill.creator_id,
)
return "OK"