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:
0
api/__init__.py
Normal file
0
api/__init__.py
Normal file
18
api/core/deps.py
Normal file
18
api/core/deps.py
Normal 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
13
api/core/notifications.py
Normal 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
52
api/main.py
Normal 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
3
api/routes/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .pally import router as pally_router
|
||||
|
||||
routers = [pally_router]
|
||||
78
api/routes/pally.py
Normal file
78
api/routes/pally.py
Normal 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"
|
||||
Reference in New Issue
Block a user